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

This commit is contained in:
anhefti 2021-07-26 09:05:59 +02:00
commit bc9b3de297
50 changed files with 526 additions and 632 deletions

View file

@ -59,6 +59,14 @@ jobs:
run: echo $SHA
env:
SHA: ${{ steps.short-sha.outputs.sha }}
-
name: Set env
run: echo "TAG_NAME=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
-
name: Test tag name
run: |
echo $TAG_NAME
echo ${{ env.TAG_NAME }}
-
name: Checkout repository
uses: actions/checkout@v2
@ -77,12 +85,12 @@ jobs:
restore-keys: ${{ runner.os }}-m2
-
name: Build with Maven
run: mvn clean install -Dmaven.test.skip=true -Dsebserver-version="${{ env.SHA }}"
run: mvn clean install -Dmaven.test.skip=true -Dsebserver-version="${{ env.TAG_NAME }}"
env:
sebserver-version: ${{ env.SHA }}
sebserver-version: ${{ env.TAG_NAME }}
-
name: Simplify package name
run: mv target/seb-server-${{ env.SHA }}.jar target/seb-server.jar
run: mv target/seb-server-${{ env.TAG_NAME }}.jar target/seb-server.jar
-
uses: actions/upload-artifact@v2
with:

View file

@ -37,7 +37,7 @@ import ch.ethz.seb.sebserver.gbl.profile.ProdWebServiceProfile;
* SEB-Server uses Spring's profiles to consequently separate sub-components of the webservice
* and GUI and can be used to start the components on separate servers or within the same
* server instance. Additional to the usual profiles like dev, prod, test there are combining
* profiles like dev-ws, dev-gui and prod-ws, prod-gui */
* profiles like dev-ws, dev-gui and prod-ws, prod-gui test */
@SpringBootApplication(exclude = {
UserDetailsServiceAutoConfiguration.class,
})

View file

@ -383,7 +383,6 @@ public final class Result<T> {
@Override
public String toString() {
throw new RuntimeException("Result.toString is probably called by mistake !!!");
//return "Result [value=" + this.value + ", error=" + this.error + "]";
}
public interface TryCatchSupplier<T> {

View file

@ -92,7 +92,7 @@ public class ExamFormIndicators implements TemplateComposer {
entityKey.modelId))
.withEmptyMessage(INDICATOR_EMPTY_LIST_MESSAGE)
.withMarkup()
.withPaging(5)
.withPaging(100)
.hideNavigation()
.withColumn(new ColumnDefinition<>(
Domain.INDICATOR.ATTR_NAME,

View file

@ -12,6 +12,7 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.function.BooleanSupplier;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.eclipse.swt.widgets.Composite;
import org.joda.time.DateTime;
@ -100,6 +101,8 @@ public class QuizLookupList implements TemplateComposer {
new LocTextKey("sebserver.quizdiscovery.quiz.import.out.dated");
private final static LocTextKey TEXT_KEY_CONFIRM_EXISTING =
new LocTextKey("sebserver.quizdiscovery.quiz.import.existing.confirm");
private final static LocTextKey TEXT_KEY_EXISTING =
new LocTextKey("sebserver.quizdiscovery.quiz.import.existing");
private final static String TEXT_KEY_ADDITIONAL_ATTR_PREFIX =
"sebserver.quizdiscovery.quiz.details.additional.";
@ -147,6 +150,7 @@ public class QuizLookupList implements TemplateComposer {
final CurrentUser currentUser = this.resourceService.getCurrentUser();
final RestService restService = this.resourceService.getRestService();
final I18nSupport i18nSupport = this.resourceService.getI18nSupport();
final Long institutionId = currentUser.get().institutionId;
// content page layout with title
final Composite content = this.widgetFactory.defaultPageLayout(
@ -242,10 +246,10 @@ public class QuizLookupList implements TemplateComposer {
.publish(false)
.newAction(ActionDefinition.QUIZ_DISCOVERY_EXAM_IMPORT)
.withConfirm(importQuizConfirm(table, restService))
.withConfirm(importQuizConfirm(institutionId, table, restService))
.withSelect(
table.getGrantedSelection(currentUser, NO_MODIFY_PRIVILEGE_ON_OTHER_INSTITUTION),
action -> this.importQuizData(action, table),
action -> this.importQuizData(institutionId, action, table, restService),
EMPTY_SELECTION_TEXT)
.publishIf(() -> examGrant.im(), false);
}
@ -257,6 +261,7 @@ public class QuizLookupList implements TemplateComposer {
}
private Function<PageAction, LocTextKey> importQuizConfirm(
final Long institutionId,
final EntityTable<QuizData> table,
final RestService restService) {
@ -264,12 +269,15 @@ public class QuizLookupList implements TemplateComposer {
action.getSingleSelection();
final QuizData selectedROWData = table.getSingleSelectedROWData();
final Collection<EntityKey> existingImports = restService.getBuilder(CheckExamImported.class)
final Collection<Long> existingImports = restService.getBuilder(CheckExamImported.class)
.withURIVariable(API.PARAM_MODEL_ID, selectedROWData.id)
.call()
.getOrThrow();
.getOrThrow()
.stream()
.map(key -> Long.valueOf(key.modelId))
.collect(Collectors.toList());
if (existingImports != null && !existingImports.isEmpty()) {
if (existingImports != null && !existingImports.isEmpty() && !existingImports.contains(institutionId)) {
return TEXT_KEY_CONFIRM_EXISTING;
} else {
return null;
@ -278,8 +286,10 @@ public class QuizLookupList implements TemplateComposer {
}
private PageAction importQuizData(
final Long institutionId,
final PageAction action,
final EntityTable<QuizData> table) {
final EntityTable<QuizData> table,
final RestService restService) {
action.getSingleSelection();
final QuizData selectedROWData = table.getSingleSelectedROWData();
@ -291,6 +301,18 @@ public class QuizLookupList implements TemplateComposer {
}
}
final Collection<Long> existingImports = restService.getBuilder(CheckExamImported.class)
.withURIVariable(API.PARAM_MODEL_ID, selectedROWData.id)
.call()
.getOrThrow()
.stream()
.map(key -> Long.valueOf(key.modelId))
.collect(Collectors.toList());
if (existingImports.contains(institutionId)) {
throw new PageMessageException(TEXT_KEY_EXISTING);
}
return action
.withEntityKey(action.getSingleSelection())
.withParentEntityKey(new EntityKey(selectedROWData.lmsSetupId, EntityType.LMS_SETUP))

View file

@ -232,7 +232,7 @@ public class UserAccountList implements TemplateComposer {
.newAction(ActionDefinition.USER_ACCOUNT_TOGGLE_ACTIVITY)
.withExec(this.pageService.activationToggleActionFunction(table, EMPTY_SELECTION_TEXT_KEY))
.withConfirm(this.pageService.confirmDeactivation(table))
.publishIf(() -> userGrant.m(), false);
.publishIf(() -> userGrant.im(), false);
}
private PageAction editAction(final PageAction pageAction) {

View file

@ -169,7 +169,10 @@ public class PasswordFieldBuilder implements InputFieldBuilder {
public String getValue() {
final CharSequence pwd = this.control.getValue();
if (StringUtils.isNotBlank(pwd)) {
return this.cryptor.encrypt(pwd).toString();
return this.cryptor
.encrypt(pwd)
.getOrThrow()
.toString();
}
return StringUtils.EMPTY;

View file

@ -11,6 +11,7 @@ package ch.ethz.seb.sebserver.gui.service.push;
import java.util.function.Consumer;
import org.eclipse.rap.rwt.service.ServerPushSession;
import org.eclipse.swt.SWTException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy;
@ -70,6 +71,8 @@ public class ServerPushService {
business.accept(context);
doUpdate(context, update);
} catch (final SWTException swte) {
log.error("Disposed GUI widget(s) while update: {}", swte.getMessage());
} catch (final Exception e) {
log.error("Unexpected error while do business for server push service", e);
context.internalStop = context.errorHandler.apply(e);

View file

@ -28,7 +28,7 @@ public class DeleteUserAccount extends RestCall<EntityProcessingReport> {
public DeleteUserAccount() {
super(new TypeKey<>(
CallType.ACTIVATION_DEACTIVATE,
CallType.DELETE,
EntityType.USER,
new TypeReference<EntityProcessingReport>() {
}),

View file

@ -195,10 +195,6 @@ public final class ClientConnectionTable {
this.table.layout();
}
// public int getUpdateErrors() {
// return this.updateErrors;
// }
public WidgetFactory getWidgetFactory() {
return this.pageService.getWidgetFactory();
}
@ -337,6 +333,7 @@ public final class ClientConnectionTable {
try {
// TODO forceUpdateAll doeasn't work on distributed
if (this.statusFilterChanged || this.forceUpdateAll || needsSync) {
this.toDelete.clear();
this.toDelete.addAll(this.tableMapping.keySet());

View file

@ -33,7 +33,7 @@ public interface ClientEventLastPingMapper {
@SelectProvider(type = SqlProviderAdapter.class, method = "select")
@ResultType(ClientEventLastPingRecord.class)
@ConstructorArgs({
@Arg(column = "client_connection_id", javaType = Long.class, jdbcType = JdbcType.BIGINT),
@Arg(column = "id", javaType = Long.class, jdbcType = JdbcType.BIGINT),
@Arg(column = "server_time", javaType = Long.class, jdbcType = JdbcType.BIGINT),
})
Collection<ClientEventLastPingRecord> selectMany(SelectStatementProvider select);
@ -42,7 +42,7 @@ public interface ClientEventLastPingMapper {
return SelectDSL.selectWithMapper(
this::selectMany,
ClientEventRecordDynamicSqlSupport.clientConnectionId.as("client_connection_id"),
ClientEventRecordDynamicSqlSupport.clientConnectionId.as("id"),
ClientEventRecordDynamicSqlSupport.serverTime.as("server_time"))
.from(ClientEventRecordDynamicSqlSupport.clientEventRecord);
@ -50,14 +50,14 @@ public interface ClientEventLastPingMapper {
final class ClientEventLastPingRecord {
public final Long connectionId;
public final Long id;
public final Long lastPingTime;
public ClientEventLastPingRecord(
final Long client_connection_id,
final Long id,
final Long server_time) {
this.connectionId = client_connection_id;
this.id = id;
this.lastPingTime = server_time;
}
}

View file

@ -42,6 +42,11 @@ public interface ClientConnectionDAO extends
unless = "#result.hasError()")
Result<Collection<String>> getConnectionTokens(Long examId);
@CacheEvict(cacheNames = CONNECTION_TOKENS_CACHE, key = "#examId")
default void evictConnectionTokenCache(final Long examId) {
}
/** Get a list of all connection tokens of all connections (no matter what state)
* of an exam.
*
@ -151,6 +156,8 @@ public interface ClientConnectionDAO extends
* @return Result refer to true if the given ClientConnection is up to date */
Result<Boolean> isUpToDate(ClientConnection clientConnection);
Result<Set<String>> getClientConnectionsOutOfSyc(Long examId, Set<Long> timestamps);
/** Indicates if the client connection for given exam and connection token is
* in a ready state to send instructions.
*

View file

@ -17,6 +17,7 @@ import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
import ch.ethz.seb.sebserver.gbl.model.session.ClientNotification;
import ch.ethz.seb.sebserver.gbl.model.session.ExtendedClientEvent;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientEventRecord;
public interface ClientEventDAO extends EntityDAO<ClientEvent, ClientEvent> {
@ -54,4 +55,10 @@ public interface ClientEventDAO extends EntityDAO<ClientEvent, ClientEvent> {
* @return Result refer to the confirmed notification or to en error when happened */
Result<ClientNotification> confirmPendingNotification(Long notificationId, Long clientConnectionId);
Result<ClientEventRecord> initPingEvent(Long connectionId);
void updatePingEvent(ClientEventRecord pingRecord);
Result<Long> getLastPing(Long pk);
}

View file

@ -0,0 +1,31 @@
/*
* 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;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
public class DuplicateResourceException extends RuntimeException {
private static final long serialVersionUID = 2935680103812281185L;
/** The entity key of the resource that was requested */
public final EntityKey entityKey;
public DuplicateResourceException(final EntityType entityType, final String modelId) {
super("Resource " + entityType + " with ID: " + modelId + " already exists");
this.entityKey = new EntityKey(modelId, entityType);
}
public DuplicateResourceException(final EntityType entityType, final String modelId, final Throwable cause) {
super("Resource " + entityType + " with ID: " + modelId + " not found", cause);
this.entityKey = new EntityKey(modelId, entityType);
}
}

View file

@ -44,7 +44,11 @@ public interface ExamDAO extends ActivatableEntityDAO<Exam, Exam>, BulkActionSup
* happened */
Result<Collection<Long>> allIdsOfInstitution(Long institutionId);
Result<Collection<Long>> allByQuizId(String quizId);
/** Get all institution ids for that a specified exam for given quiz id already exists
*
* @param quizId The quiz or external identifier of the exam (LMS)
* @return Result refer to a collection of primary keys of the institutions or to an error when happened */
Result<Collection<Long>> allInstitutionIdsByQuizId(String quizId);
/** Updates the exam status for specified exam
*

View file

@ -13,6 +13,7 @@ import static org.mybatis.dynamic.sql.SqlBuilder.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
@ -553,9 +554,10 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO {
}
@Override
@Transactional(readOnly = true)
public Result<Boolean> isUpToDate(final ClientConnection clientConnection) {
return Result.tryCatch(() -> this.clientConnectionRecordMapper
.selectByExample()
.countByExample()
.where(
ClientConnectionRecordDynamicSqlSupport.connectionToken,
SqlBuilder.isEqualTo(clientConnection.connectionToken))
@ -563,10 +565,31 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO {
ClientConnectionRecordDynamicSqlSupport.updateTime,
SqlBuilder.isEqualTo(clientConnection.updateTime))
.build()
.execute()
.stream()
.findFirst()
.isPresent());
.execute() > 0);
}
@Override
@Transactional(readOnly = true)
public Result<Set<String>> getClientConnectionsOutOfSyc(final Long examId, final Set<Long> timestamps) {
return Result.tryCatch(() -> {
final Set<String> result = new HashSet<>();
this.clientConnectionRecordMapper
.selectByExample()
.where(
ClientConnectionRecordDynamicSqlSupport.examId,
SqlBuilder.isEqualTo(examId))
.build()
.execute()
.stream()
.forEach(cc -> {
if (!timestamps.contains(cc.getUpdateTime())) {
result.add(cc.getConnectionToken());
}
});
return result;
});
}
@Override

View file

@ -14,7 +14,6 @@ import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
@ -243,7 +242,8 @@ public class ClientEventDAOImpl implements ClientEventDAO {
public Result<ClientNotification> confirmPendingNotification(final Long notificationId,
final Long clientConnectionId) {
return Result.tryCatch(() -> {
final Long pk = this.clientEventRecordMapper.selectIdsByExample()
final Long pk = this.clientEventRecordMapper
.selectIdsByExample()
.where(ClientEventRecordDynamicSqlSupport.id, isEqualTo(notificationId))
.and(ClientEventRecordDynamicSqlSupport.type, isEqualTo(EventType.NOTIFICATION.id))
.build()
@ -295,23 +295,99 @@ public class ClientEventDAOImpl implements ClientEventDAO {
@Transactional
public Result<Collection<EntityKey>> delete(final Set<EntityKey> all) {
return Result.tryCatch(() -> {
return all
final List<Long> pks = all
.stream()
.map(EntityKey::getModelId)
.map(Long::parseLong)
.map(pk -> {
final int deleted = this.clientEventRecordMapper.deleteByPrimaryKey(pk);
if (deleted == 1) {
return new EntityKey(String.valueOf(pk), EntityType.CLIENT_EVENT);
} else {
return null;
}
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
this.clientEventRecordMapper
.deleteByExample()
.where(ClientEventRecordDynamicSqlSupport.id, isIn(pks))
.build()
.execute();
return pks
.stream()
.map(pk -> new EntityKey(String.valueOf(pk), EntityType.CLIENT_EVENT))
.collect(Collectors.toList());
// return all
// .stream()
// .map(EntityKey::getModelId)
// .map(Long::parseLong)
// .map(pk -> {
// final int deleted = this.clientEventRecordMapper.deleteByPrimaryKey(pk);
// if (deleted == 1) {
// return new EntityKey(String.valueOf(pk), EntityType.CLIENT_EVENT);
// } else {
// return null;
// }
// })
// .filter(Objects::nonNull)
// .collect(Collectors.toList());
});
}
@Override
@Transactional
public Result<ClientEventRecord> initPingEvent(final Long connectionId) {
return Result.tryCatch(() -> {
final List<ClientEventRecord> lastPingRec = this.clientEventRecordMapper
.selectByExample()
.where(ClientEventRecordDynamicSqlSupport.clientConnectionId, isEqualTo(connectionId))
.and(ClientEventRecordDynamicSqlSupport.type, isEqualTo(EventType.LAST_PING.id))
.build()
.execute();
if (lastPingRec != null && !lastPingRec.isEmpty()) {
return lastPingRec.get(0);
}
final long millisecondsNow = Utils.getMillisecondsNow();
final ClientEventRecord clientEventRecord = new ClientEventRecord();
clientEventRecord.setClientConnectionId(connectionId);
clientEventRecord.setType(EventType.LAST_PING.id);
clientEventRecord.setClientTime(millisecondsNow);
clientEventRecord.setServerTime(millisecondsNow);
this.clientEventRecordMapper.insert(clientEventRecord);
try {
return this.clientEventRecordMapper
.selectByExample()
.where(ClientEventRecordDynamicSqlSupport.clientConnectionId, isEqualTo(connectionId))
.and(ClientEventRecordDynamicSqlSupport.type, isEqualTo(EventType.LAST_PING.id))
.build()
.execute()
.get(0);
} catch (final Exception e) {
return clientEventRecord;
}
});
}
@Override
@Transactional
public void updatePingEvent(final ClientEventRecord pingRecord) {
try {
this.clientEventRecordMapper.updateByPrimaryKeySelective(pingRecord);
} catch (final Exception e) {
log.error("Failed to update ping event: {}", e.getMessage());
}
}
@Override
@Transactional(readOnly = true)
public Result<Long> getLastPing(final Long pk) {
return Result.tryCatch(() -> this.clientEventRecordMapper
.selectByPrimaryKey(pk)
.getServerTime());
}
private Result<ClientEventRecord> recordById(final Long id) {
return Result.tryCatch(() -> {

View file

@ -59,6 +59,7 @@ import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.LmsSetupRecordDyn
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;
@ -139,7 +140,7 @@ public class ExamDAOImpl implements ExamDAO {
}
@Override
public Result<Collection<Long>> allByQuizId(final String quizId) {
public Result<Collection<Long>> allInstitutionIdsByQuizId(final String quizId) {
return Result.tryCatch(() -> {
return this.examRecordMapper.selectByExample()
.where(
@ -151,7 +152,7 @@ public class ExamDAOImpl implements ExamDAO {
.build()
.execute()
.stream()
.map(rec -> rec.getId())
.map(rec -> rec.getInstitutionId())
.collect(Collectors.toList());
});
}
@ -331,23 +332,9 @@ public class ExamDAOImpl implements ExamDAO {
// 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
// open the existing. otherwise create new one if requested
// if the same institution tries to import an exam that already exists throw an error
if (exam.institutionId.equals(examRecord.getInstitutionId())) {
final ExamRecord newRecord = new ExamRecord(
examRecord.getId(),
null, null, null, null, null,
(exam.type != null) ? exam.type.name() : ExamType.UNDEFINED.name(),
null, // quitPassword
null, // browser keys
null, // status
null, // lmsSebRestriction (deprecated)
null, // updating
null, // lastUpdate
BooleanUtils.toIntegerObject(exam.active));
this.examRecordMapper.updateByPrimaryKeySelective(newRecord);
return this.examRecordMapper.selectByPrimaryKey(examRecord.getId());
throw new DuplicateResourceException(EntityType.EXAM, exam.externalId);
}
}

View file

@ -577,7 +577,10 @@ public class SEBClientConfigDAOImpl implements SEBClientConfigDAO {
EntityType.SEB_CLIENT_CONFIGURATION,
configId,
SEBClientConfig.ATTR_FALLBACK_PASSWORD,
this.clientCredentialService.encrypt(sebClientConfig.fallbackPassword).toString());
this.clientCredentialService
.encrypt(sebClientConfig.fallbackPassword)
.getOrThrow()
.toString());
} else {
this.additionalAttributesDAO.delete(
EntityType.SEB_CLIENT_CONFIGURATION,
@ -590,7 +593,10 @@ public class SEBClientConfigDAOImpl implements SEBClientConfigDAO {
EntityType.SEB_CLIENT_CONFIGURATION,
configId,
SEBClientConfig.ATTR_QUIT_PASSWORD,
this.clientCredentialService.encrypt(sebClientConfig.quitPassword).toString());
this.clientCredentialService
.encrypt(sebClientConfig.quitPassword)
.getOrThrow()
.toString());
} else {
this.additionalAttributesDAO.delete(
EntityType.SEB_CLIENT_CONFIGURATION,

View file

@ -476,12 +476,8 @@ public class MoodleCourseAccess extends AbstractCourseAccess {
final Map<String, CourseData> finalCourseDataRef = courseData;
courseQuizData.quizzes
.forEach(quiz -> {
final CourseData course = finalCourseDataRef.get(quiz.course);
if (course != null) {
course.quizzes.add(quiz);
}
});
.stream()
.forEach(quiz -> fillSelectedQuizzes(quizIds, finalCourseDataRef, quiz));
final String urlPrefix = (lmsSetup.lmsApiUrl.endsWith(Constants.URL_PATH_SEPARATOR))
? lmsSetup.lmsApiUrl + MOODLE_QUIZ_START_URL_PATH
@ -510,6 +506,27 @@ public class MoodleCourseAccess extends AbstractCourseAccess {
}
}
private void fillSelectedQuizzes(
final Set<String> quizIds,
final Map<String, CourseData> finalCourseDataRef,
final CourseQuiz quiz) {
try {
final CourseData course = finalCourseDataRef.get(quiz.course);
if (course != null) {
final String internalQuizId = getInternalQuizId(
quiz.course_module,
course.id,
course.short_name,
course.idnumber);
if (quizIds.contains(internalQuizId)) {
course.quizzes.add(quiz);
}
}
} catch (final Exception e) {
log.error("Failed to verify selected quiz for course: {}", e.getMessage());
}
}
private Collection<CourseData> getCoursesForIds(
final MoodleAPIRestTemplate restTemplate,
final Set<String> ids) {

View file

@ -517,7 +517,10 @@ public class ExamConfigXMLParser extends DefaultHandler {
attribute.id,
listIndex,
StringUtils.isNotBlank(value)
? this.cryptor.encrypt(value + Constants.IMPORTED_PASSWORD_MARKER).toString()
? this.cryptor
.encrypt(value + Constants.IMPORTED_PASSWORD_MARKER)
.getOrThrow()
.toString()
: value);
}

View file

@ -133,7 +133,10 @@ public class StringConverter implements AttributeValueConverter {
// decrypt internally encrypted password and hash it for export
// NOTE: see special case description in ExamConfigXMLParser.createConfigurationValue
final String plainText = this.clientCredentialService.decrypt(value).toString();
final String plainText = this.clientCredentialService
.decrypt(value)
.getOrThrow()
.toString();
if (plainText.endsWith(Constants.IMPORTED_PASSWORD_MARKER)) {
return plainText.replace(Constants.IMPORTED_PASSWORD_MARKER, StringUtils.EMPTY);
} else {

View file

@ -27,8 +27,9 @@ public interface ClientIndicator extends IndicatorValue {
*
* @param indicatorDefinition The indicator definition that defines type and thresholds of the indicator
* @param connectionId the connection identifier to that this ClientIndicator is associated to
* @param active indicates whether the connection is still an a active state or not
* @param cachingEnabled defines whether indicator value caching is enabled or not. */
void init(Indicator indicatorDefinition, Long connectionId, boolean cachingEnabled);
void init(Indicator indicatorDefinition, Long connectionId, boolean active, boolean cachingEnabled);
/** Get the exam identifier of the client connection of this ClientIndicator
*

View file

@ -75,7 +75,7 @@ public class ClientConnectionDataInternal extends ClientConnectionData {
@Override
@JsonProperty(ATTR_MISSING_PING)
public Boolean getMissingPing() {
return this.pingIndicator.isMissingPing();
return this.pingIndicator != null && this.pingIndicator.isMissingPing();
}
@Override

View file

@ -81,6 +81,7 @@ public class ClientIndicatorFactory {
indicator.init(
indicatorDef,
clientConnection.id,
clientConnection.status.clientActiveStatus,
this.enableCaching);
result.add(indicator);
@ -105,9 +106,11 @@ public class ClientIndicatorFactory {
null,
null,
Arrays.asList(new Indicator.Threshold(5000d, null, null)));
pingIndicator.init(
indicator,
clientConnection.id,
clientConnection.status.clientActiveStatus,
this.enableCaching);
result.add(pingIndicator);
}

View file

@ -1,81 +0,0 @@
/*
* Copyright (c) 2019 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.session.impl;
import java.math.BigDecimal;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.EventType;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientEventRecordMapper;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientEventRecord;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.PingHandlingStrategy;
@Lazy
@Component
@WebServiceProfile
public class DistributedServerPingHandler implements PingHandlingStrategy {
private static final Logger log = LoggerFactory.getLogger(DistributedServerPingHandler.class);
private final ExamSessionCacheService examSessionCacheService;
private final ClientEventRecordMapper clientEventRecordMapper;
protected DistributedServerPingHandler(
final ExamSessionCacheService examSessionCacheService,
final ClientEventRecordMapper clientEventRecordMapper) {
this.examSessionCacheService = examSessionCacheService;
this.clientEventRecordMapper = clientEventRecordMapper;
}
@Override
@Transactional
public void notifyPing(final String connectionToken, final long timestamp, final int pingNumber) {
// store last ping in event
final ClientEventRecord pingRecord = this.examSessionCacheService.getPingRecord(connectionToken);
if (pingRecord != null) {
pingRecord.setClientTime(timestamp);
pingRecord.setServerTime(Utils.getMillisecondsNow());
pingRecord.setNumericValue(new BigDecimal(pingNumber));
this.clientEventRecordMapper.updateByPrimaryKeySelective(pingRecord);
}
// update ping indicators
final ClientConnectionDataInternal activeClientConnection =
this.examSessionCacheService.getClientConnection(connectionToken);
if (activeClientConnection != null) {
activeClientConnection.notifyPing(timestamp, pingNumber);
}
}
@Override
public void initForConnection(final Long connectionId, final String connectionToken) {
if (log.isDebugEnabled()) {
log.debug("Initialize distributed ping handler for connection: {}", connectionId);
}
final ClientEventRecord clientEventRecord = new ClientEventRecord();
clientEventRecord.setClientConnectionId(connectionId);
clientEventRecord.setType(EventType.LAST_PING.id);
clientEventRecord.setClientTime(Utils.getMillisecondsNow());
clientEventRecord.setServerTime(Utils.getMillisecondsNow());
this.clientEventRecordMapper.insertSelective(clientEventRecord);
}
}

View file

@ -10,25 +10,18 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl;
import java.io.ByteArrayOutputStream;
import org.mybatis.dynamic.sql.SqlBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
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.session.ClientConnection;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.EventType;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
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.ClientEventRecordDynamicSqlSupport;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientEventRecordMapper;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientEventRecord;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientConnectionDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.RemoteProctoringRoomDAO;
@ -48,7 +41,6 @@ public class ExamSessionCacheService {
public static final String CACHE_NAME_RUNNING_EXAM = "RUNNING_EXAM";
public static final String CACHE_NAME_ACTIVE_CLIENT_CONNECTION = "ACTIVE_CLIENT_CONNECTION";
public static final String CACHE_NAME_SEB_CONFIG_EXAM = "SEB_CONFIG_EXAM";
public static final String CACHE_NAME_PING_RECORD = "CACHE_NAME_PING_RECORD";
private static final Logger log = LoggerFactory.getLogger(ExamSessionCacheService.class);
@ -56,7 +48,6 @@ public class ExamSessionCacheService {
private final ClientConnectionDAO clientConnectionDAO;
private final InternalClientConnectionDataFactory internalClientConnectionDataFactory;
private final ExamConfigService sebExamConfigService;
private final ClientEventRecordMapper clientEventRecordMapper;
private final ExamUpdateHandler examUpdateHandler;
protected ExamSessionCacheService(
@ -64,7 +55,6 @@ public class ExamSessionCacheService {
final ClientConnectionDAO clientConnectionDAO,
final InternalClientConnectionDataFactory internalClientConnectionDataFactory,
final ExamConfigService sebExamConfigService,
final ClientEventRecordMapper clientEventRecordMapper,
final ExamUpdateHandler examUpdateHandler,
final RemoteProctoringRoomDAO remoteProctoringRoomDAO) {
@ -72,7 +62,6 @@ public class ExamSessionCacheService {
this.clientConnectionDAO = clientConnectionDAO;
this.internalClientConnectionDataFactory = internalClientConnectionDataFactory;
this.sebExamConfigService = sebExamConfigService;
this.clientEventRecordMapper = clientEventRecordMapper;
this.examUpdateHandler = examUpdateHandler;
}
@ -125,7 +114,7 @@ public class ExamSessionCacheService {
}
public boolean isRunning(final Exam exam) {
if (exam == null) {
if (exam == null || !exam.active) {
return false;
}
@ -202,56 +191,13 @@ public class ExamSessionCacheService {
}
}
@Cacheable(
cacheNames = CACHE_NAME_PING_RECORD,
key = "#connectionToken",
unless = "#result == null")
@Transactional
public ClientEventRecord getPingRecord(final String connectionToken) {
if (log.isDebugEnabled()) {
log.debug("Verify ClientConnection for ping record to cache by connectionToken: {}", connectionToken);
}
final ClientConnection clientConnection = getClientConnectionByToken(connectionToken);
if (clientConnection == null) {
return null;
} else {
try {
return this.clientEventRecordMapper.selectByExample()
.where(
ClientEventRecordDynamicSqlSupport.clientConnectionId,
SqlBuilder.isEqualTo(clientConnection.getId()))
.and(
ClientEventRecordDynamicSqlSupport.type,
SqlBuilder.isEqualTo(EventType.LAST_PING.id))
.build()
.execute()
.stream()
.collect(Utils.toSingleton());
} catch (final Exception e) {
log.error("Unexpected error: ", e);
return null;
}
}
}
@CacheEvict(
cacheNames = CACHE_NAME_PING_RECORD,
key = "#connectionToken")
public void evictPingRecord(final String connectionToken) {
if (log.isTraceEnabled()) {
log.trace("Eviction of ReusableClientEventRecord from cache for connection token: {}", connectionToken);
}
}
private ClientConnection getClientConnectionByToken(final String connectionToken) {
final Result<ClientConnection> result = this.clientConnectionDAO
.byConnectionToken(connectionToken);
if (result.hasError()) {
log.error("Failed to find/load ClientConnection with connectionToken {}", connectionToken,
log.error("Failed to find/load ClientConnection with connectionToken {}",
connectionToken,
result.getError());
return null;
}

View file

@ -15,12 +15,11 @@ import java.util.Collection;
import java.util.Collections;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.function.Function;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.apache.commons.lang3.BooleanUtils;
import org.mybatis.dynamic.sql.SqlBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
@ -38,9 +37,6 @@ import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.ClientConnectionMinMapper;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.ClientConnectionMinMapper.ClientConnectionMinRecord;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientConnectionRecordDynamicSqlSupport;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientConnectionDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamConfigurationMapDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO;
@ -49,7 +45,6 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.IndicatorDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.indicator.IndicatorDistributedRequestCache;
@Lazy
@Service
@ -59,37 +54,33 @@ public class ExamSessionServiceImpl implements ExamSessionService {
private static final Logger log = LoggerFactory.getLogger(ExamSessionServiceImpl.class);
private final ClientConnectionDAO clientConnectionDAO;
private final ClientConnectionMinMapper clientConnectionMinMapper;
private final IndicatorDAO indicatorDAO;
private final ExamSessionCacheService examSessionCacheService;
private final ExamDAO examDAO;
private final ExamConfigurationMapDAO examConfigurationMapDAO;
private final CacheManager cacheManager;
private final SEBRestrictionService sebRestrictionService;
private final IndicatorDistributedRequestCache indicatorDistributedRequestCache;
private final boolean distributedSetup;
private long lastConnectionTokenCacheUpdate = 0;
protected ExamSessionServiceImpl(
final ExamSessionCacheService examSessionCacheService,
final ClientConnectionMinMapper clientConnectionMinMapper,
final ExamDAO examDAO,
final ExamConfigurationMapDAO examConfigurationMapDAO,
final ClientConnectionDAO clientConnectionDAO,
final IndicatorDAO indicatorDAO,
final CacheManager cacheManager,
final SEBRestrictionService sebRestrictionService,
final IndicatorDistributedRequestCache indicatorDistributedRequestCache,
@Value("${sebserver.webservice.distributed:false}") final boolean distributedSetup) {
this.examSessionCacheService = examSessionCacheService;
this.clientConnectionMinMapper = clientConnectionMinMapper;
this.examDAO = examDAO;
this.examConfigurationMapDAO = examConfigurationMapDAO;
this.clientConnectionDAO = clientConnectionDAO;
this.cacheManager = cacheManager;
this.indicatorDAO = indicatorDAO;
this.sebRestrictionService = sebRestrictionService;
this.indicatorDistributedRequestCache = indicatorDistributedRequestCache;
this.distributedSetup = distributedSetup;
}
@ -324,21 +315,11 @@ public class ExamSessionServiceImpl implements ExamSessionService {
final ClientConnectionDataInternal activeClientConnection = this.examSessionCacheService
.getClientConnection(connectionToken);
if (activeClientConnection == null) {
throw new NoSuchElementException("Client Connection with token: " + connectionToken);
}
if (this.distributedSetup) {
final Boolean upToDate = this.clientConnectionDAO
.isUpToDate(activeClientConnection.clientConnection)
.getOr(false);
if (!upToDate) {
this.examSessionCacheService.evictClientConnection(connectionToken);
return this.examSessionCacheService.getClientConnection(connectionToken);
}
}
return activeClientConnection;
});
@ -349,35 +330,20 @@ public class ExamSessionServiceImpl implements ExamSessionService {
final Long examId,
final Predicate<ClientConnectionData> filter) {
if (this.distributedSetup) {
return Result.tryCatch(() -> {
// if we run in distributed mode, we have to get the connection tokens of the exam
// always from the persistent storage and update the client connection cache
// before by remove out-dated client connection. This is done within the update_time
// of the client connection record that is set on every update in the persistent
// storage. So if the update_time of the cached client connection doesen't match the
// update_time from persistent, we need to flush this particular client connection from the cache
this.indicatorDistributedRequestCache.evictPingTimes(examId);
return Result.tryCatch(() -> this.clientConnectionMinMapper.selectByExample()
.where(
ClientConnectionRecordDynamicSqlSupport.examId,
SqlBuilder.isEqualTo(examId))
.build()
.execute()
.stream()
.map(this.distributedClientConnectionUpdateFunction(filter))
.filter(filter)
.collect(Collectors.toList()));
updateClientConnections(examId);
} else {
return Result.tryCatch(() -> this.clientConnectionDAO
return this.clientConnectionDAO
.getConnectionTokens(examId)
.getOrThrow()
.stream()
.map(this.examSessionCacheService::getClientConnection)
.map(token -> getConnectionData(token).getOr(null))
.filter(Objects::nonNull)
.filter(filter)
.collect(Collectors.toList()));
}
.collect(Collectors.toList());
});
}
@Override
@ -415,32 +381,43 @@ public class ExamSessionServiceImpl implements ExamSessionService {
.forEach(token -> {
// evict client connection
this.examSessionCacheService.evictClientConnection(token);
// evict also cached ping record
this.examSessionCacheService.evictPingRecord(token);
});
return exam;
});
}
private Function<ClientConnectionMinRecord, ClientConnectionDataInternal> distributedClientConnectionUpdateFunction(
final Predicate<ClientConnectionData> filter) {
// If we are in a distributed setup the active connection token cache get flushed
// at least every second. This allows caching over multiple monitoring requests but
// ensure an update every second for new incoming connections
private void updateClientConnections(final Long examId) {
return cd -> {
ClientConnectionDataInternal clientConnection = this.examSessionCacheService
.getClientConnection(cd.connection_token);
try {
if (this.distributedSetup &&
System.currentTimeMillis() - this.lastConnectionTokenCacheUpdate > Constants.SECOND_IN_MILLIS) {
if (filter.test(clientConnection)) {
if (cd.update_time != null &&
!cd.update_time.equals(clientConnection.clientConnection.updateTime)) {
// go trough all client connection and update the ones that not up to date
this.clientConnectionDAO.evictConnectionTokenCache(examId);
this.examSessionCacheService.evictClientConnection(cd.connection_token);
clientConnection = this.examSessionCacheService
.getClientConnection(cd.connection_token);
}
final Set<Long> timestamps = this.clientConnectionDAO
.getConnectionTokens(examId)
.getOrThrow()
.stream()
.map(this.examSessionCacheService::getClientConnection)
.filter(Objects::nonNull)
.map(cc -> cc.getClientConnection().updateTime)
.collect(Collectors.toSet());
this.clientConnectionDAO.getClientConnectionsOutOfSyc(examId, timestamps)
.getOrElse(() -> Collections.emptySet())
.stream()
.forEach(this.examSessionCacheService::evictClientConnection);
this.lastConnectionTokenCacheUpdate = System.currentTimeMillis();
}
return clientConnection;
};
} catch (final Exception e) {
log.error("Unexpected error while trying to update client connections: ", e);
}
}
}

View file

@ -1,44 +0,0 @@
/*
* Copyright (c) 2019 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.session.impl;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.webservice.WebserviceInfo;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.PingHandlingStrategy;
@Lazy
@Service
@WebServiceProfile
public class PingHandlingStrategyFactory {
private final SingleServerPingHandler singleServerPingHandler;
private final DistributedServerPingHandler distributedServerPingHandler;
private final WebserviceInfo webserviceInfo;
protected PingHandlingStrategyFactory(
final SingleServerPingHandler singleServerPingHandler,
final DistributedServerPingHandler distributedServerPingHandler,
final WebserviceInfo webserviceInfo) {
this.singleServerPingHandler = singleServerPingHandler;
this.distributedServerPingHandler = distributedServerPingHandler;
this.webserviceInfo = webserviceInfo;
}
public PingHandlingStrategy get() {
if (this.webserviceInfo.isDistributed()) {
return this.distributedServerPingHandler;
} else {
return this.singleServerPingHandler;
}
}
}

View file

@ -16,8 +16,6 @@ import java.util.function.Predicate;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.Cache;
@ -35,6 +33,7 @@ import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.EventType;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.WebserviceInfo;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientConnectionRecord;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientEventRecord;
@ -43,7 +42,6 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.SEBClientConfigDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamAdminService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.EventHandlingStrategy;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.PingHandlingStrategy;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientConnectionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientInstructionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientNotificationService;
@ -68,7 +66,6 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
private final CacheManager cacheManager;
private final EventHandlingStrategy eventHandlingStrategy;
private final ClientConnectionDAO clientConnectionDAO;
private final PingHandlingStrategy pingHandlingStrategy;
private final SEBClientConfigDAO sebClientConfigDAO;
private final SEBClientInstructionService sebInstructionService;
private final SEBClientNotificationService sebClientNotificationService;
@ -78,7 +75,7 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
protected SEBClientConnectionServiceImpl(
final ExamSessionService examSessionService,
final EventHandlingStrategyFactory eventHandlingStrategyFactory,
final PingHandlingStrategyFactory pingHandlingStrategyFactory,
final SEBClientConfigDAO sebClientConfigDAO,
final SEBClientInstructionService sebInstructionService,
final SEBClientNotificationService sebClientNotificationService,
@ -88,7 +85,6 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
this.examSessionCacheService = examSessionService.getExamSessionCacheService();
this.cacheManager = examSessionService.getCacheManager();
this.clientConnectionDAO = examSessionService.getClientConnectionDAO();
this.pingHandlingStrategy = pingHandlingStrategyFactory.get();
this.eventHandlingStrategy = eventHandlingStrategyFactory.get();
this.sebClientConfigDAO = sebClientConfigDAO;
this.sebInstructionService = sebInstructionService;
@ -368,11 +364,6 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
updatedClientConnection);
}
// notify ping handler about established connection
this.pingHandlingStrategy.initForConnection(
updatedClientConnection.id,
connectionToken);
return updatedClientConnection;
});
}
@ -520,7 +511,8 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
try {
final Cache cache = this.cacheManager.getCache(ExamSessionCacheService.CACHE_NAME_ACTIVE_CLIENT_CONNECTION);
final long now = DateTime.now(DateTimeZone.UTC).getMillis();
final long now = Utils.getMillisecondsNow();
final Consumer<ClientConnectionDataInternal> missingPingUpdate = missingPingUpdate(now);
this.examSessionService
.getExamDAO()
.allRunningExamIds()
@ -538,8 +530,8 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
.map(token -> cache.get(token, ClientConnectionDataInternal.class))
.filter(Objects::nonNull)
.filter(connection -> connection.pingIndicator != null &&
connection.clientConnection.status.establishedStatus)
.forEach(missingPingUpdate(now));
connection.clientConnection.status.clientActiveStatus)
.forEach(connection -> missingPingUpdate.accept(connection));
} catch (final Exception e) {
log.error("Failed to update ping events: ", e);
@ -557,7 +549,13 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
final long timestamp,
final int pingNumber) {
this.pingHandlingStrategy.notifyPing(connectionToken, timestamp, pingNumber);
final ClientConnectionDataInternal activeClientConnection =
this.examSessionCacheService.getClientConnection(connectionToken);
if (activeClientConnection != null) {
activeClientConnection.notifyPing(timestamp, pingNumber);
}
return this.sebInstructionService.getInstructionJSON(connectionToken);
}
@ -732,8 +730,6 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
private ClientConnectionDataInternal reloadConnectionCache(final String connectionToken) {
// evict cached ClientConnection
this.examSessionCacheService.evictClientConnection(connectionToken);
// evict also cached ping record
this.examSessionCacheService.evictPingRecord(connectionToken);
// and load updated ClientConnection into cache
return this.examSessionCacheService.getClientConnection(connectionToken);
}

View file

@ -48,6 +48,7 @@ public class SEBClientInstructionServiceImpl implements SEBClientInstructionServ
private static final Logger log = LoggerFactory.getLogger(SEBClientInstructionServiceImpl.class);
private static final long PERSISTENT_UPDATE_INTERVAL = 2 * Constants.SECOND_IN_MILLIS;
private static final int INSTRUCTION_QUEUE_MAX_SIZE = 10;
private static final String JSON_INST = "instruction";
private static final String JSON_ATTR = "attributes";
@ -281,9 +282,9 @@ public class SEBClientInstructionServiceImpl implements SEBClientInstructionServ
// Since the queue is empty check periodically if there are active instructions on the persistent storage
final long currentTimeMillis = System.currentTimeMillis();
if (currentTimeMillis - this.lastRefresh > Constants.SECOND_IN_MILLIS) {
if (currentTimeMillis - this.lastRefresh > PERSISTENT_UPDATE_INTERVAL) {
this.lastRefresh = currentTimeMillis;
loadInstructions(connectionToken)
loadInstructions()
.onError(error -> log.error(
"Failed load instructions from persistent storage and to refresh cache: ",
error));
@ -300,8 +301,10 @@ public class SEBClientInstructionServiceImpl implements SEBClientInstructionServ
// check if there are still queues in the cache, whether they are empty or not,
// for closed or disposed client connections and remove them from cache
synchronized (this.instructions) {
final Result<Collection<String>> result = this.clientConnectionDAO
.getInactiveConnctionTokens(this.instructions.keySet());
if (result.hasValue()) {
result.get().stream().forEach(token -> this.instructions.remove(token));
}
@ -325,12 +328,6 @@ public class SEBClientInstructionServiceImpl implements SEBClientInstructionServ
}
}
private Result<Void> loadInstructions(final String connectionToken) {
return Result.tryCatch(() -> this.clientInstructionDAO.getAllActive(connectionToken)
.getOrThrow()
.forEach(this::putToCacheIfAbsent));
}
private Result<Void> loadInstructions() {
return Result.tryCatch(() -> this.clientInstructionDAO.getAllActive()
.getOrThrow()

View file

@ -1,44 +0,0 @@
/*
* Copyright (c) 2019 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.session.impl;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.PingHandlingStrategy;
@Lazy
@Component
@WebServiceProfile
public class SingleServerPingHandler implements PingHandlingStrategy {
private final ExamSessionCacheService examSessionCacheService;
protected SingleServerPingHandler(final ExamSessionCacheService examSessionCacheService) {
this.examSessionCacheService = examSessionCacheService;
}
@Override
public void notifyPing(final String connectionToken, final long timestamp, final int pingNumber) {
// update ping indicators
final ClientConnectionDataInternal activeClientConnection =
this.examSessionCacheService.getClientConnection(connectionToken);
if (activeClientConnection != null) {
activeClientConnection.notifyPing(timestamp, pingNumber);
}
}
@Override
public void initForConnection(final Long connectionId, final String connectionToken) {
// nothing to do here
}
}

View file

@ -8,18 +8,22 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.indicator;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.DateTimeUtils;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.model.exam.Indicator;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ClientIndicator;
public abstract class AbstractClientIndicator implements ClientIndicator {
private static final long PERSISTENT_UPDATE_INTERVAL = Constants.SECOND_IN_MILLIS;
protected Long indicatorId;
protected Long examId;
protected Long connectionId;
protected boolean cachingEnabled;
protected boolean active = true;
protected long lastPersistentUpdate = 0;
protected boolean valueInitializes = false;
protected double currentValue = Double.NaN;
@ -28,11 +32,13 @@ public abstract class AbstractClientIndicator implements ClientIndicator {
public void init(
final Indicator indicatorDefinition,
final Long connectionId,
final boolean active,
final boolean cachingEnabled) {
this.indicatorId = (indicatorDefinition != null) ? indicatorDefinition.id : -1;
this.examId = (indicatorDefinition != null) ? indicatorDefinition.examId : -1;
this.connectionId = connectionId;
this.active = active;
this.cachingEnabled = cachingEnabled;
}
@ -58,11 +64,20 @@ public abstract class AbstractClientIndicator implements ClientIndicator {
@Override
public double getValue() {
if (!this.valueInitializes || !this.cachingEnabled) {
this.currentValue = computeValueAt(DateTime.now(DateTimeZone.UTC).getMillis());
final long now = DateTimeUtils.currentTimeMillis();
if (!this.valueInitializes) {
this.currentValue = computeValueAt(now);
this.lastPersistentUpdate = now;
this.valueInitializes = true;
}
if (!this.cachingEnabled && this.active) {
if (now - this.lastPersistentUpdate > PERSISTENT_UPDATE_INTERVAL) {
this.currentValue = computeValueAt(now);
this.lastPersistentUpdate = now;
}
}
return this.currentValue;
}

View file

@ -37,8 +37,14 @@ public abstract class AbstractLogIndicator extends AbstractClientIndicator {
}
@Override
public void init(final Indicator indicatorDefinition, final Long connectionId, final boolean cachingEnabled) {
super.init(indicatorDefinition, connectionId, cachingEnabled);
public void init(
final Indicator indicatorDefinition,
final Long connectionId,
final boolean active,
final boolean cachingEnabled) {
super.init(indicatorDefinition, connectionId, active, cachingEnabled);
if (indicatorDefinition == null || StringUtils.isBlank(indicatorDefinition.tags)) {
this.tags = null;
} else {

View file

@ -56,14 +56,11 @@ public abstract class AbstractLogLevelCountIndicator extends AbstractLogIndicato
@Override
public double computeValueAt(final long timestamp) {
try {
// TODO to boost performance here within a distributed setup, invent a new cache for all log count values
// of the running exam. So all indicators get the values from cache and only one single SQL call
// is needed for one update.
// This cache then is only valid for one (GUI) update cycle and the cache must to be flushed before
final Long errors = this.clientEventRecordMapper.countByExample()
final Long errors = this.clientEventRecordMapper
.countByExample()
.where(ClientEventRecordDynamicSqlSupport.clientConnectionId, isEqualTo(this.connectionId))
.and(ClientEventRecordDynamicSqlSupport.type, isIn(this.eventTypeIds))
.and(ClientEventRecordDynamicSqlSupport.serverTime, isLessThan(timestamp))

View file

@ -64,11 +64,6 @@ public abstract class AbstractLogNumberIndicator extends AbstractLogIndicator {
public double computeValueAt(final long timestamp) {
try {
// TODO to boost performance here within a distributed setup, invent a new cache for all log count values
// of the running exam. So all indicators get the values from cache and only one single SQL call
// is needed for one update.
// This cache then is only valid for one (GUI) update cycle and the cache must to be flushed before
final List<ClientEventRecord> execute = this.clientEventRecordMapper.selectByExample()
.where(ClientEventRecordDynamicSqlSupport.clientConnectionId, isEqualTo(this.connectionId))
.and(ClientEventRecordDynamicSqlSupport.type, isIn(this.eventTypeIds))

View file

@ -13,54 +13,61 @@ import java.util.EnumSet;
import java.util.Set;
import org.joda.time.DateTime;
import org.joda.time.DateTimeUtils;
import org.joda.time.DateTimeZone;
import com.fasterxml.jackson.annotation.JsonIgnore;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.model.exam.Indicator;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.EventType;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.ClientEventExtensionMapper;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientEventRecord;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientEventDAO;
public abstract class AbstractPingIndicator extends AbstractClientIndicator {
private static final long INTERVAL_FOR_PERSISTENT_UPDATE = Constants.SECOND_IN_MILLIS;
private final Set<EventType> EMPTY_SET = Collections.unmodifiableSet(EnumSet.noneOf(EventType.class));
protected final ClientEventExtensionMapper clientEventExtensionMapper;
protected final IndicatorDistributedRequestCache indicatorDistributedRequestCache;
protected final ClientEventDAO clientEventDAO;
protected long pingLatency;
protected int pingCount = 0;
protected int pingNumber = 0;
protected AbstractPingIndicator(
final ClientEventExtensionMapper clientEventExtensionMapper,
final IndicatorDistributedRequestCache indicatorDistributedRequestCache) {
private long lastUpdate = 0;
protected ClientEventRecord pingRecord = null;
protected AbstractPingIndicator(final ClientEventDAO clientEventDAO) {
super();
this.clientEventExtensionMapper = clientEventExtensionMapper;
this.indicatorDistributedRequestCache = indicatorDistributedRequestCache;
this.clientEventDAO = clientEventDAO;
}
@Override
public void init(
final Indicator indicatorDefinition,
final Long connectionId,
final boolean active,
final boolean cachingEnabled) {
super.init(indicatorDefinition, connectionId, active, cachingEnabled);
if (!this.cachingEnabled) {
this.pingRecord = this.clientEventDAO
.initPingEvent(this.connectionId)
.getOr(null);
}
}
public final void notifyPing(final long timestamp, final int pingNumber) {
final long now = DateTime.now(DateTimeZone.UTC).getMillis();
this.pingLatency = now - timestamp;
super.currentValue = now;
this.pingCount++;
this.pingNumber = pingNumber;
}
super.lastPersistentUpdate = now;
@Override
public final double computeValueAt(final long timestamp) {
if (this.cachingEnabled) {
return timestamp;
} else {
try {
return this.indicatorDistributedRequestCache
.getPingTimes(this.examId)
.getOrDefault(this.connectionId, 0L);
if (!this.cachingEnabled && this.pingRecord != null) {
} catch (final Exception e) {
return Double.NaN;
// Update last ping time on persistent storage
final long millisecondsNow = DateTimeUtils.currentTimeMillis();
if (millisecondsNow - this.lastUpdate > INTERVAL_FOR_PERSISTENT_UPDATE) {
this.pingRecord.setClientTime(timestamp);
this.pingRecord.setServerTime(millisecondsNow);
this.clientEventDAO.updatePingEvent(this.pingRecord);
this.lastUpdate = millisecondsNow;
}
}
}
@ -70,16 +77,6 @@ public abstract class AbstractPingIndicator extends AbstractClientIndicator {
return this.EMPTY_SET;
}
@JsonIgnore
public int getPingCount() {
return this.pingCount;
}
@JsonIgnore
public int getPingNumber() {
return this.pingNumber;
}
public abstract ClientEventRecord updateLogEvent(final long now);
}

View file

@ -30,8 +30,13 @@ public class BatteryStatusIndicator extends AbstractLogNumberIndicator {
}
@Override
public void init(final Indicator indicatorDefinition, final Long connectionId, final boolean cachingEnabled) {
super.init(indicatorDefinition, connectionId, cachingEnabled);
public void init(
final Indicator indicatorDefinition,
final Long connectionId,
final boolean active,
final boolean cachingEnabled) {
super.init(indicatorDefinition, connectionId, active, cachingEnabled);
super.tags = new String[] { API.LOG_EVENT_TAG_BATTERY_STATUS };
}

View file

@ -1,71 +0,0 @@
/*
* 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.session.impl.indicator;
import static org.mybatis.dynamic.sql.SqlBuilder.isEqualTo;
import java.util.stream.Collectors;
import org.ehcache.impl.internal.concurrent.ConcurrentHashMap;
import org.mybatis.dynamic.sql.SqlBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.EventType;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.ClientEventLastPingMapper;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientConnectionRecordDynamicSqlSupport;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientEventRecordDynamicSqlSupport;
@Lazy
@Service
@WebServiceProfile
public class IndicatorDistributedRequestCache {
public static final String LAST_PING_TIME_CACHE = "LAST_PING_TIME_CACHE";
private static final Logger log = LoggerFactory.getLogger(IndicatorDistributedRequestCache.class);
private final ClientEventLastPingMapper clientEventLastPingMapper;
public IndicatorDistributedRequestCache(final ClientEventLastPingMapper clientEventLastPingMapper) {
this.clientEventLastPingMapper = clientEventLastPingMapper;
}
@Cacheable(
cacheNames = LAST_PING_TIME_CACHE,
key = "#examId",
unless = "#result == null")
public ConcurrentHashMap<Long, Long> getPingTimes(final Long examId) {
return new ConcurrentHashMap<>(this.clientEventLastPingMapper.selectByExample()
.join(ClientConnectionRecordDynamicSqlSupport.clientConnectionRecord)
.on(
ClientEventRecordDynamicSqlSupport.clientConnectionId,
SqlBuilder.equalTo(ClientConnectionRecordDynamicSqlSupport.id))
.where(ClientConnectionRecordDynamicSqlSupport.examId, isEqualTo(examId))
.and(ClientEventRecordDynamicSqlSupport.type, isEqualTo(EventType.LAST_PING.id))
.build()
.execute()
.stream().collect(Collectors.toMap(rec -> rec.connectionId, rec -> rec.lastPingTime)));
}
@CacheEvict(
cacheNames = LAST_PING_TIME_CACHE,
key = "#examId")
public void evictPingTimes(final Long examId) {
if (log.isDebugEnabled()) {
log.debug("Evict LAST_PING_TIME_CACHE for examId: {}", examId);
}
}
}

View file

@ -11,8 +11,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.indicator;
import java.math.BigDecimal;
import java.util.Comparator;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.DateTimeUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
@ -27,9 +26,9 @@ import ch.ethz.seb.sebserver.gbl.model.exam.Indicator;
import ch.ethz.seb.sebserver.gbl.model.exam.Indicator.IndicatorType;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.EventType;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.ClientEventExtensionMapper;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientEventRecord;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientEventDAO;
@Lazy
@Component(IndicatorType.Names.LAST_PING)
@ -38,22 +37,30 @@ public final class PingIntervalClientIndicator extends AbstractPingIndicator {
private static final Logger log = LoggerFactory.getLogger(PingIntervalClientIndicator.class);
long pingErrorThreshold;
boolean missingPing = false;
boolean hidden = false;
// This is the default ping error threshold that is set if the threshold cannot be get
// from the ping threshold settings. If the last ping is older then this interval back in time
// then the ping is considered and marked as missing
private static final long DEFAULT_PING_ERROR_THRESHOLD = Constants.SECOND_IN_MILLIS * 5;
public PingIntervalClientIndicator(
final ClientEventExtensionMapper clientEventExtensionMapper,
final IndicatorDistributedRequestCache indicatorDistributedRequestCache) {
private long pingErrorThreshold;
private boolean missingPing = false;
private boolean hidden = false;
super(clientEventExtensionMapper, indicatorDistributedRequestCache);
public PingIntervalClientIndicator(final ClientEventDAO clientEventDAO) {
super(clientEventDAO);
this.cachingEnabled = true;
this.currentValue = computeValueAt(Utils.getMillisecondsNow());
}
@Override
public void init(final Indicator indicatorDefinition, final Long connectionId, final boolean cachingEnabled) {
super.init(indicatorDefinition, connectionId, cachingEnabled);
public void init(
final Indicator indicatorDefinition,
final Long connectionId,
final boolean active,
final boolean cachingEnabled) {
super.init(indicatorDefinition, connectionId, active, cachingEnabled);
this.currentValue = computeValueAt(DateTimeUtils.currentTimeMillis());
try {
indicatorDefinition
@ -64,12 +71,13 @@ public final class PingIntervalClientIndicator extends AbstractPingIndicator {
} catch (final Exception e) {
log.error("Failed to initialize pingErrorThreshold: {}", e.getMessage());
this.pingErrorThreshold = Constants.SECOND_IN_MILLIS * 5;
this.pingErrorThreshold = DEFAULT_PING_ERROR_THRESHOLD;
}
if (!cachingEnabled) {
try {
this.missingPing = this.pingErrorThreshold < getValue();
final double value = getValue();
this.missingPing = this.pingErrorThreshold < value;
} catch (final Exception e) {
log.error("Failed to initialize missingPing: {}", e.getMessage());
this.missingPing = false;
@ -100,7 +108,7 @@ public final class PingIntervalClientIndicator extends AbstractPingIndicator {
@Override
public double getValue() {
final long now = DateTime.now(DateTimeZone.UTC).getMillis();
final long now = DateTimeUtils.currentTimeMillis();
return now - super.getValue();
}
@ -114,6 +122,33 @@ public final class PingIntervalClientIndicator extends AbstractPingIndicator {
}
@Override
public final double computeValueAt(final long timestamp) {
if (!this.cachingEnabled && super.pingRecord != null) {
// if this indicator is not missing ping
if (!this.isMissingPing()) {
final Result<Long> lastPing = this.clientEventDAO
.getLastPing(super.pingRecord.getId());
if (!lastPing.hasError()) {
if (Double.isNaN(this.currentValue)) {
return lastPing.get().doubleValue();
}
return Math.max(this.currentValue, lastPing.get().doubleValue());
} else {
log.error("Failed to get last ping from persistent: {}", lastPing.getError().getMessage());
}
}
return this.currentValue;
}
return !this.valueInitializes ? timestamp : this.currentValue;
}
@Override
public ClientEventRecord updateLogEvent(final long now) {
final long value = now - (long) super.currentValue;

View file

@ -30,8 +30,13 @@ public class WLANStatusIndicator extends AbstractLogNumberIndicator {
}
@Override
public void init(final Indicator indicatorDefinition, final Long connectionId, final boolean cachingEnabled) {
super.init(indicatorDefinition, connectionId, cachingEnabled);
public void init(
final Indicator indicatorDefinition,
final Long connectionId,
final boolean active,
final boolean cachingEnabled) {
super.init(indicatorDefinition, connectionId, active, cachingEnabled);
super.tags = new String[] { API.LOG_EVENT_TAG_WLAN_STATUS };
}

View file

@ -280,25 +280,11 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
proctoringRoom.id)
.getOrThrow();
final Result<RemoteProctoringRoom> townhallRoomResult = this.remoteProctoringRoomDAO
.getTownhallRoom(cc.getExamId());
applyProcotringInstruction(
cc.getExamId(),
cc.getConnectionToken())
.getOrThrow();
if (townhallRoomResult.hasValue()) {
final RemoteProctoringRoom townhallRoom = townhallRoomResult.get();
applyProcotringInstruction(
cc.getExamId(),
cc.getConnectionToken(),
townhallRoom.name,
townhallRoom.subject)
.getOrThrow();
} else {
applyProcotringInstruction(
cc.getExamId(),
cc.getConnectionToken(),
proctoringRoom.name,
proctoringRoom.subject)
.getOrThrow();
}
} catch (final Exception e) {
log.error("Failed to assign connection to collecting room: {}", cc, e);
}
@ -601,9 +587,7 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
private Result<Void> applyProcotringInstruction(
final Long examId,
final String connectionToken,
final String roomName,
final String subject) {
final String connectionToken) {
return Result.tryCatch(() -> {
final ProctoringServiceSettings settings = this.examAdminService
@ -614,10 +598,32 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
.getExamProctoringService(settings.serverType)
.getOrThrow();
sendJoinCollectingRoomInstructions(
settings,
Arrays.asList(connectionToken),
examProctoringService);
final Result<RemoteProctoringRoom> townhallRoomResult = this.remoteProctoringRoomDAO
.getTownhallRoom(examId);
if (townhallRoomResult.hasValue()) {
final RemoteProctoringRoom townhallRoom = townhallRoomResult.get();
final ProctoringRoomConnection roomConnection = examProctoringService.getClientRoomConnection(
settings,
connectionToken,
townhallRoom.name,
townhallRoom.subject)
.getOrThrow();
sendJoinInstruction(
examId,
connectionToken,
roomConnection,
examProctoringService);
} else {
sendJoinCollectingRoomInstructions(
settings,
Arrays.asList(connectionToken),
examProctoringService);
}
});
}

View file

@ -270,7 +270,9 @@ public class ExamAPI_V1_Controller {
final String instructionConfirm = request.getParameter(API.EXAM_API_PING_INSTRUCTION_CONFIRM);
if (log.isTraceEnabled()) {
log.trace("****************** SEB client connection: {}", connectionToken);
log.trace("****************** SEB client connection: {} ip: ",
connectionToken,
getClientAddress(request));
}
if (instructionConfirm != null) {
@ -352,32 +354,6 @@ public class ExamAPI_V1_Controller {
final ServletOutputStream outputStream = response.getOutputStream();
// try {
//
// final ClientConnectionData connection = this.examSessionService
// .getConnectionData(connectionToken)
// .getOrThrow();
//
// // exam integrity check
// if (connection.clientConnection.examId == null ||
// !this.examSessionService.isExamRunning(connection.clientConnection.examId)) {
//
// log.error("Missing exam identifier or requested exam is not running for connection: {}",
// connection);
// throw new IllegalStateException("Missing exam identifier or requested exam is not running");
// }
// } catch (final Exception e) {
//
// log.error("Unexpected error: ", e);
//
// final APIMessage errorMessage = APIMessage.ErrorMessage.GENERIC.of(e.getMessage());
// outputStream.write(Utils.toByteArray(this.jsonMapper.writeValueAsString(errorMessage)));
// response.setStatus(HttpStatus.BAD_REQUEST.value());
// outputStream.flush();
// outputStream.close();
// return;
// }
try {
this.examSessionService

View file

@ -228,10 +228,10 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId) {
checkReadPrivilege(institutionId);
return this.examDAO.allByQuizId(modelId)
return this.examDAO.allInstitutionIdsByQuizId(modelId)
.map(ids -> ids
.stream()
.map(id -> new EntityKey(id, EntityType.EXAM))
.map(id -> new EntityKey(id, EntityType.INSTITUTION))
.collect(Collectors.toList()))
.getOrThrow();
}
@ -256,10 +256,6 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
.checkExamConsistency(modelId)
.getOrThrow();
if (includeRestriction) {
// TODO include seb restriction check and status
}
return result;
}

View file

@ -11,7 +11,7 @@ logging.level.ch=INFO
logging.level.org.springframework.cache=INFO
logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl=DEBUG
logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.session=DEBUG
logging.level.ch.ethz.seb.sebserver.webservice.weblayer.api.ExamAPI_V1_Controller=TRACE
#logging.level.ch.ethz.seb.sebserver.webservice.weblayer.api.ExamAPI_V1_Controller=TRACE
sebserver.http.client.connect-timeout=150000
sebserver.http.client.connection-request-timeout=100000

View file

@ -22,6 +22,7 @@ spring.datasource.url=jdbc:mariadb://${datastore.mariadb.server.address}:${datas
spring.flyway.enabled=true
spring.flyway.locations=classpath:config/sql/base
spring.flyway.cleanDisabled=true
spring.flyway.ignoreIgnoredMigrations=true
spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
spring.datasource.hikari.initializationFailTimeout=3000
spring.datasource.hikari.connectionTimeout=30000

View file

@ -82,17 +82,6 @@
</resources>
</cache>
<cache alias="LAST_PING_TIME_CACHE">
<key-type>java.lang.Long</key-type>
<value-type>org.ehcache.impl.internal.concurrent.ConcurrentHashMap</value-type>
<expiry>
<ttl unit="hours">24</ttl>
</expiry>
<resources>
<heap unit="entries">10</heap>
</resources>
</cache>
<cache alias="QUIZ_DATA_CACHE">
<key-type>java.lang.String</key-type>
<value-type>ch.ethz.seb.sebserver.gbl.model.exam.QuizData</value-type>

View file

@ -380,6 +380,7 @@ sebserver.quizdiscovery.action.import=Import as Exam
sebserver.quizdiscovery.quiz.import.out.dated=The Selected LMS exam is already finished and can't be imported
sebserver.quizdiscovery.action.details=Show LMS Exam Details
sebserver.quizdiscovery.quiz.import.existing.confirm=This course was already imported and importing it twice may lead to<br/> unexpected behavior within automated SEB restriction on LMS.<br/><br/> Do you want to import this course as exam anyway?
sebserver.quizdiscovery.quiz.import.existing=This course was already imported as an exam.<br/>Please find it in the Exam section.
sebserver.quizdiscovery.quiz.details.title=LMS Exam Details
sebserver.quizdiscovery.quiz.details.institution=Institution
@ -595,7 +596,7 @@ sebserver.exam.indicator.type.WLAN_STATUS=WiFi Status
sebserver.exam.indicator.type.description.LAST_PING=This indicator shows the time in milliseconds since<br/> the last ping has been received from a SEB Client.<br/>This indicator can be used to track a SEB Client connection and indicate connection loss.<br/><br/>The value is in milliseconds.
sebserver.exam.indicator.type.description.ERROR_COUNT=This indicator shows the number of error log messages that<br/> has been received from a SEB Client.<br/>This indicator can be used to track errors of connected SEB Clients<br/><br/>The value is a natural number.
sebserver.exam.indicator.type.description.WARN_COUNT=This indicator shows the number of warn log messages that<br/> has been received from a SEB Client.<br/>This indicator can be used to track warnings of connected SEB Clients<br/><br/>The value is a natural number.
sebserver.exam.indicator.type.description.INFO_COUNT=This indicator shows the number of warn log messages that<br/> has been received from a SEB Client.<br/>This indicator can be used to track warnings of connected SEB Clients<br/><br/>The value is a natural number.
sebserver.exam.indicator.type.description.INFO_COUNT=This indicator shows the number of info log messages that<br/> has been received from a SEB Client.<br/>This indicator can be used to track warnings of connected SEB Clients<br/><br/>The value is a natural number.
sebserver.exam.indicator.type.description.BATTERY_STATUS=This indicator shows the percentage of the battery load level of a SEB Client.
sebserver.exam.indicator.type.description.WLAN_STATUS=This indicator shows the percentage of the WiFi connection status of a SEB Client.
@ -1429,7 +1430,7 @@ sebserver.examconfig.props.label.proctoringDetectTalkingDisplay=Feedback for Can
sebserver.examconfig.props.label.proctoringDetectTalkingDisplay.tooltips=
sebserver.examconfig.props.label.remoteProctoringViewShow=Proctoring View Display Policy
sebserver.examconfig.props.label.remoteProctoringViewShow.tooltip=
sebserver.examconfig.props.label.remoteProctoringViewShow.tooltip=Controls the general policy for the initial display of the proctoring Window in SEB.<br/>Theses setting have no effect on the proctor's broadcast or chat function.
sebserver.examconfig.props.label.remoteProctoringViewShow.0=Never
sebserver.examconfig.props.label.remoteProctoringViewShow.0.tooltip=
sebserver.examconfig.props.label.remoteProctoringViewShow.1=Allow to Show
@ -1448,23 +1449,24 @@ sebserver.examconfig.props.group.zoom_features.tooltip=
sebserver.examconfig.props.group.zoom_controls=Zoom User Controls
sebserver.examconfig.props.group.zoom_controls.tooltip=When the proctoring view is disabled (see its display policy on left), users can mute and unmute audio and video manually.<br/>Audio and video streams can be disabled globally with the settings below.
sebserver.examconfig.props.label.zoomAudioMuted=Audio Initially Muted
sebserver.examconfig.props.label.zoomAudioMuted.tooltip=
sebserver.examconfig.props.label.zoomAudioMuted.tooltip=If this option is activated, client proctoring windows will start muted.<br/>The SEB users then can start/stop their audio individually using the respective button in the Zoom windows in SEB.<br/>If the Proctoring View Display Policy is set to "Never", this option will have no effect.
sebserver.examconfig.props.label.zoomAudioOnly=Audio Only
sebserver.examconfig.props.label.zoomAudioOnly.tooltip=
sebserver.examconfig.props.label.zoomAudioOnly.tooltip=If this option is activated, cameras on SEB clients are not active until the proctoring window is opened by the proctor<br/>(and they are deactivated if it is closed again!)
sebserver.examconfig.props.label.zoomEnable=Enable Zoom
sebserver.examconfig.props.label.zoomEnable.tooltip=
sebserver.examconfig.props.label.zoomFeatureFlagChat=Enable Chat
sebserver.examconfig.props.label.zoomFeatureFlagChat.tooltip=
sebserver.examconfig.props.label.zoomFeatureFlagChat.tooltip=Allows SEB users to use Zoom's chat function in SEB, if the proctor enables chat in the proctoring window on SEB Server
sebserver.examconfig.props.label.zoomFeatureFlagCloseCaptions=Enable Close Captions
sebserver.examconfig.props.label.zoomFeatureFlagCloseCaptions.tooltip=
sebserver.examconfig.props.label.zoomFeatureFlagDisplayingMeetingName=Display Meeting Name
sebserver.examconfig.props.label.zoomFeatureFlagDisplayMeetingName.tooltip
sebserver.examconfig.props.label.zoomFeatureFlagCloseCaptions.tooltip=Not yet implemented
sebserver.examconfig.props.label.zoomFeatureFlagDisplayMeetingName=Display Meeting Name
sebserver.examconfig.props.label.zoomFeatureFlagDisplayMeetingName.tooltip=Not yet implemented
sebserver.examconfig.props.label.zoomFeatureFlagRaiseHand=Enable Raise Hand
sebserver.examconfig.props.label.zoomFeatureFlagRaiseHand.tooltip=
sebserver.examconfig.props.label.zoomFeatureFlagRaiseHand.tooltip=Allows SEB users to use Zoom's raise hand function in SEB during a broadcast or chat
sebserver.examconfig.props.label.zoomFeatureFlagRecording=Allow Recording
sebserver.examconfig.props.label.zoomFeatureFlagRecording.tooltip=
sebserver.examconfig.props.label.zoomFeatureFlagTileView=Allow Tile View
sebserver.examconfig.props.label.zoomFeatureFlagTileView.tooltip=Note: Disabling Allow Tile View is not yet functional in this version.
sebserver.examconfig.props.label.zoomFeatureFlagTileView.tooltip=Not yet implemented
sebserver.examconfig.props.label.zoomRoom=Room
sebserver.examconfig.props.label.zoomRoom.tooltip=
sebserver.examconfig.props.label.zoomServerURL=Server URL
@ -1480,16 +1482,16 @@ sebserver.examconfig.props.label.zoomUserInfoDisplayName.tooltip=
sebserver.examconfig.props.label.zoomUserInfoEMail=Info Mail
sebserver.examconfig.props.label.zoomUserInfoEMail.tooltip=
sebserver.examconfig.props.label.zoomVideoMuted=Video Initially Muted
sebserver.examconfig.props.label.zoomVideoMuted.tooltip=
sebserver.examconfig.props.label.zoomVideoMuted.tooltip=If this option is activated, a video broadcasts or chat starts with SEB clients cameras off.<br/>The SEB users then can start/stop their video individually using the respective button in the Zoom windows in SEB
sebserver.examconfig.props.label.zoomReceiveAudio=Receive Audio
sebserver.examconfig.props.label.zoomReceiveAudio.tooltip=Global settings for receiving and sending audio/video streams.<br/>Streams can also be enabled/disabled by SEB Server during the exam session.
sebserver.examconfig.props.label.zoomReceiveAudio.tooltip=Not yet implemented
sebserver.examconfig.props.label.zoomReceiveVideo=Receive Video
sebserver.examconfig.props.label.zoomReceiveVideo.tooltip=Global settings for receiving and sending audio/video streams.<br/>Streams can also be enabled/disabled by SEB Server during the exam session.
sebserver.examconfig.props.label.zoomReceiveVideo.tooltip=Controls video connection if Proctoring View Display Policy allows initial display of Zoom window on SEB startup
sebserver.examconfig.props.label.zoomSendAudio=Send Audio
sebserver.examconfig.props.label.zoomSendAudio.tooltip=Global settings for receiving and sending audio/video streams.<br/>Streams can also be enabled/disabled by SEB Server during the exam session.
sebserver.examconfig.props.label.zoomSendAudio.tooltip=Not yet implemented
sebserver.examconfig.props.label.zoomSendVideo=Send Video
sebserver.examconfig.props.label.zoomSendVideo.tooltip=Global settings for receiving and sending audio/video streams.<br/>Streams can also be enabled/disabled by SEB Server during the exam session.
sebserver.examconfig.props.label.zoomSendVideo.tooltip=Not yet implemented
sebserver.examconfig.props.label.showProctoringViewButton=Show Proctoring Button

View file

@ -38,7 +38,6 @@ import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientConnectionRe
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientEventRecord;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.ClientConnectionDataInternal;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.ExamSessionCacheService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.indicator.AbstractPingIndicator;
@Sql(scripts = { "classpath:schema-test.sql", "classpath:data-test.sql", "classpath:data-test-additional.sql" })
public class SebConnectionTest extends ExamAPIIntegrationTester {
@ -468,20 +467,15 @@ public class SebConnectionTest extends ExamAPIIntegrationTester {
assertFalse(ccdi.indicatorValues.isEmpty());
final IndicatorValue pingIndicator = ccdi.indicatorValues.iterator().next();
assertTrue(pingIndicator.getType() == IndicatorType.LAST_PING);
assertEquals("0", String.valueOf(((AbstractPingIndicator) pingIndicator).getPingNumber()));
super.sendPing(accessToken, connectionToken, 1);
Thread.sleep(200);
assertEquals("1", String.valueOf(((AbstractPingIndicator) pingIndicator).getPingNumber()));
super.sendPing(accessToken, connectionToken, 2);
Thread.sleep(200);
assertEquals("2", String.valueOf(((AbstractPingIndicator) pingIndicator).getPingNumber()));
super.sendPing(accessToken, connectionToken, 3);
Thread.sleep(200);
assertEquals("3", String.valueOf(((AbstractPingIndicator) pingIndicator).getPingNumber()));
super.sendPing(accessToken, connectionToken, 5);
Thread.sleep(200);
assertEquals("5", String.valueOf(((AbstractPingIndicator) pingIndicator).getPingNumber()));
}
@Test
@ -531,7 +525,6 @@ public class SebConnectionTest extends ExamAPIIntegrationTester {
assertFalse(ccdi.indicatorValues.isEmpty());
final IndicatorValue pingIndicator = ccdi.indicatorValues.iterator().next();
assertTrue(pingIndicator.getType() == IndicatorType.LAST_PING);
assertEquals("0", String.valueOf(((AbstractPingIndicator) pingIndicator).getPingNumber()));
MockHttpServletResponse sendEvent = super.sendEvent(
accessToken,

View file

@ -18,7 +18,7 @@ import org.mockito.Mockito;
import com.fasterxml.jackson.core.JsonProcessingException;
import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.ClientEventExtensionMapper;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientEventDAO;
public class PingIntervalClientIndicatorTest {
@ -32,10 +32,10 @@ public class PingIntervalClientIndicatorTest {
DateTimeUtils.setCurrentMillisProvider(() -> 1L);
final ClientEventExtensionMapper clientEventExtensionMapper = Mockito.mock(ClientEventExtensionMapper.class);
final ClientEventDAO clientEventDAO = Mockito.mock(ClientEventDAO.class);
final PingIntervalClientIndicator pingIntervalClientIndicator =
new PingIntervalClientIndicator(clientEventExtensionMapper, null);
new PingIntervalClientIndicator(clientEventDAO);
assertEquals("0.0", String.valueOf(pingIntervalClientIndicator.getValue()));
}
@ -44,10 +44,10 @@ public class PingIntervalClientIndicatorTest {
DateTimeUtils.setCurrentMillisProvider(() -> 1L);
final ClientEventExtensionMapper clientEventExtensionMapper = Mockito.mock(ClientEventExtensionMapper.class);
final ClientEventDAO clientEventDAO = Mockito.mock(ClientEventDAO.class);
final PingIntervalClientIndicator pingIntervalClientIndicator =
new PingIntervalClientIndicator(clientEventExtensionMapper, null);
new PingIntervalClientIndicator(clientEventDAO);
assertEquals("0.0", String.valueOf(pingIntervalClientIndicator.getValue()));
DateTimeUtils.setCurrentMillisProvider(() -> 10L);
@ -59,10 +59,10 @@ public class PingIntervalClientIndicatorTest {
public void testSerialization() throws JsonProcessingException {
DateTimeUtils.setCurrentMillisProvider(() -> 1L);
final ClientEventExtensionMapper clientEventExtensionMapper = Mockito.mock(ClientEventExtensionMapper.class);
final ClientEventDAO clientEventDAO = Mockito.mock(ClientEventDAO.class);
final PingIntervalClientIndicator pingIntervalClientIndicator =
new PingIntervalClientIndicator(clientEventExtensionMapper, null);
new PingIntervalClientIndicator(clientEventDAO);
final JSONMapper jsonMapper = new JSONMapper();
final String json = jsonMapper.writeValueAsString(pingIntervalClientIndicator);
assertEquals("{\"indicatorValue\":0.0,\"indicatorType\":\"LAST_PING\"}", json);