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 run: echo $SHA
env: env:
SHA: ${{ steps.short-sha.outputs.sha }} 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 name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v2
@ -77,12 +85,12 @@ jobs:
restore-keys: ${{ runner.os }}-m2 restore-keys: ${{ runner.os }}-m2
- -
name: Build with Maven 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: env:
sebserver-version: ${{ env.SHA }} sebserver-version: ${{ env.TAG_NAME }}
- -
name: Simplify package 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 uses: actions/upload-artifact@v2
with: 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 * 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 * 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 * 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 = { @SpringBootApplication(exclude = {
UserDetailsServiceAutoConfiguration.class, UserDetailsServiceAutoConfiguration.class,
}) })

View file

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

View file

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

View file

@ -12,6 +12,7 @@ import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.function.BooleanSupplier; import java.util.function.BooleanSupplier;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors;
import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Composite;
import org.joda.time.DateTime; import org.joda.time.DateTime;
@ -100,6 +101,8 @@ public class QuizLookupList implements TemplateComposer {
new LocTextKey("sebserver.quizdiscovery.quiz.import.out.dated"); new LocTextKey("sebserver.quizdiscovery.quiz.import.out.dated");
private final static LocTextKey TEXT_KEY_CONFIRM_EXISTING = private final static LocTextKey TEXT_KEY_CONFIRM_EXISTING =
new LocTextKey("sebserver.quizdiscovery.quiz.import.existing.confirm"); 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 = private final static String TEXT_KEY_ADDITIONAL_ATTR_PREFIX =
"sebserver.quizdiscovery.quiz.details.additional."; "sebserver.quizdiscovery.quiz.details.additional.";
@ -147,6 +150,7 @@ public class QuizLookupList implements TemplateComposer {
final CurrentUser currentUser = this.resourceService.getCurrentUser(); final CurrentUser currentUser = this.resourceService.getCurrentUser();
final RestService restService = this.resourceService.getRestService(); final RestService restService = this.resourceService.getRestService();
final I18nSupport i18nSupport = this.resourceService.getI18nSupport(); final I18nSupport i18nSupport = this.resourceService.getI18nSupport();
final Long institutionId = currentUser.get().institutionId;
// content page layout with title // content page layout with title
final Composite content = this.widgetFactory.defaultPageLayout( final Composite content = this.widgetFactory.defaultPageLayout(
@ -242,10 +246,10 @@ public class QuizLookupList implements TemplateComposer {
.publish(false) .publish(false)
.newAction(ActionDefinition.QUIZ_DISCOVERY_EXAM_IMPORT) .newAction(ActionDefinition.QUIZ_DISCOVERY_EXAM_IMPORT)
.withConfirm(importQuizConfirm(table, restService)) .withConfirm(importQuizConfirm(institutionId, table, restService))
.withSelect( .withSelect(
table.getGrantedSelection(currentUser, NO_MODIFY_PRIVILEGE_ON_OTHER_INSTITUTION), table.getGrantedSelection(currentUser, NO_MODIFY_PRIVILEGE_ON_OTHER_INSTITUTION),
action -> this.importQuizData(action, table), action -> this.importQuizData(institutionId, action, table, restService),
EMPTY_SELECTION_TEXT) EMPTY_SELECTION_TEXT)
.publishIf(() -> examGrant.im(), false); .publishIf(() -> examGrant.im(), false);
} }
@ -257,6 +261,7 @@ public class QuizLookupList implements TemplateComposer {
} }
private Function<PageAction, LocTextKey> importQuizConfirm( private Function<PageAction, LocTextKey> importQuizConfirm(
final Long institutionId,
final EntityTable<QuizData> table, final EntityTable<QuizData> table,
final RestService restService) { final RestService restService) {
@ -264,12 +269,15 @@ public class QuizLookupList implements TemplateComposer {
action.getSingleSelection(); action.getSingleSelection();
final QuizData selectedROWData = table.getSingleSelectedROWData(); 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) .withURIVariable(API.PARAM_MODEL_ID, selectedROWData.id)
.call() .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; return TEXT_KEY_CONFIRM_EXISTING;
} else { } else {
return null; return null;
@ -278,8 +286,10 @@ public class QuizLookupList implements TemplateComposer {
} }
private PageAction importQuizData( private PageAction importQuizData(
final Long institutionId,
final PageAction action, final PageAction action,
final EntityTable<QuizData> table) { final EntityTable<QuizData> table,
final RestService restService) {
action.getSingleSelection(); action.getSingleSelection();
final QuizData selectedROWData = table.getSingleSelectedROWData(); 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 return action
.withEntityKey(action.getSingleSelection()) .withEntityKey(action.getSingleSelection())
.withParentEntityKey(new EntityKey(selectedROWData.lmsSetupId, EntityType.LMS_SETUP)) .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) .newAction(ActionDefinition.USER_ACCOUNT_TOGGLE_ACTIVITY)
.withExec(this.pageService.activationToggleActionFunction(table, EMPTY_SELECTION_TEXT_KEY)) .withExec(this.pageService.activationToggleActionFunction(table, EMPTY_SELECTION_TEXT_KEY))
.withConfirm(this.pageService.confirmDeactivation(table)) .withConfirm(this.pageService.confirmDeactivation(table))
.publishIf(() -> userGrant.m(), false); .publishIf(() -> userGrant.im(), false);
} }
private PageAction editAction(final PageAction pageAction) { private PageAction editAction(final PageAction pageAction) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -42,6 +42,11 @@ public interface ClientConnectionDAO extends
unless = "#result.hasError()") unless = "#result.hasError()")
Result<Collection<String>> getConnectionTokens(Long examId); 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) /** Get a list of all connection tokens of all connections (no matter what state)
* of an exam. * of an exam.
* *
@ -151,6 +156,8 @@ public interface ClientConnectionDAO extends
* @return Result refer to true if the given ClientConnection is up to date */ * @return Result refer to true if the given ClientConnection is up to date */
Result<Boolean> isUpToDate(ClientConnection clientConnection); 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 /** Indicates if the client connection for given exam and connection token is
* in a ready state to send instructions. * 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.ClientNotification;
import ch.ethz.seb.sebserver.gbl.model.session.ExtendedClientEvent; import ch.ethz.seb.sebserver.gbl.model.session.ExtendedClientEvent;
import ch.ethz.seb.sebserver.gbl.util.Result; 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> { 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 */ * @return Result refer to the confirmed notification or to en error when happened */
Result<ClientNotification> confirmPendingNotification(Long notificationId, Long clientConnectionId); 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 */ * happened */
Result<Collection<Long>> allIdsOfInstitution(Long institutionId); 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 /** 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.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.function.Function; import java.util.function.Function;
@ -553,9 +554,10 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO {
} }
@Override @Override
@Transactional(readOnly = true)
public Result<Boolean> isUpToDate(final ClientConnection clientConnection) { public Result<Boolean> isUpToDate(final ClientConnection clientConnection) {
return Result.tryCatch(() -> this.clientConnectionRecordMapper return Result.tryCatch(() -> this.clientConnectionRecordMapper
.selectByExample() .countByExample()
.where( .where(
ClientConnectionRecordDynamicSqlSupport.connectionToken, ClientConnectionRecordDynamicSqlSupport.connectionToken,
SqlBuilder.isEqualTo(clientConnection.connectionToken)) SqlBuilder.isEqualTo(clientConnection.connectionToken))
@ -563,10 +565,31 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO {
ClientConnectionRecordDynamicSqlSupport.updateTime, ClientConnectionRecordDynamicSqlSupport.updateTime,
SqlBuilder.isEqualTo(clientConnection.updateTime)) SqlBuilder.isEqualTo(clientConnection.updateTime))
.build() .build()
.execute() .execute() > 0);
.stream() }
.findFirst()
.isPresent()); @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 @Override

View file

@ -14,7 +14,6 @@ import java.math.BigDecimal;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -243,7 +242,8 @@ public class ClientEventDAOImpl implements ClientEventDAO {
public Result<ClientNotification> confirmPendingNotification(final Long notificationId, public Result<ClientNotification> confirmPendingNotification(final Long notificationId,
final Long clientConnectionId) { final Long clientConnectionId) {
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
final Long pk = this.clientEventRecordMapper.selectIdsByExample() final Long pk = this.clientEventRecordMapper
.selectIdsByExample()
.where(ClientEventRecordDynamicSqlSupport.id, isEqualTo(notificationId)) .where(ClientEventRecordDynamicSqlSupport.id, isEqualTo(notificationId))
.and(ClientEventRecordDynamicSqlSupport.type, isEqualTo(EventType.NOTIFICATION.id)) .and(ClientEventRecordDynamicSqlSupport.type, isEqualTo(EventType.NOTIFICATION.id))
.build() .build()
@ -295,23 +295,99 @@ public class ClientEventDAOImpl implements ClientEventDAO {
@Transactional @Transactional
public Result<Collection<EntityKey>> delete(final Set<EntityKey> all) { public Result<Collection<EntityKey>> delete(final Set<EntityKey> all) {
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
return all
final List<Long> pks = all
.stream() .stream()
.map(EntityKey::getModelId) .map(EntityKey::getModelId)
.map(Long::parseLong) .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()); .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) { private Result<ClientEventRecord> recordById(final Long id) {
return Result.tryCatch(() -> { 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.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.ResourceNotFoundException;
@ -139,7 +140,7 @@ public class ExamDAOImpl implements ExamDAO {
} }
@Override @Override
public Result<Collection<Long>> allByQuizId(final String quizId) { public Result<Collection<Long>> allInstitutionIdsByQuizId(final String quizId) {
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
return this.examRecordMapper.selectByExample() return this.examRecordMapper.selectByExample()
.where( .where(
@ -151,7 +152,7 @@ public class ExamDAOImpl implements ExamDAO {
.build() .build()
.execute() .execute()
.stream() .stream()
.map(rec -> rec.getId()) .map(rec -> rec.getInstitutionId())
.collect(Collectors.toList()); .collect(Collectors.toList());
}); });
} }
@ -331,23 +332,9 @@ public class ExamDAOImpl implements ExamDAO {
// used to save instead of create a new one // used to save instead of create a new one
if (records != null && records.size() > 0) { if (records != null && records.size() > 0) {
final ExamRecord examRecord = records.get(0); final ExamRecord examRecord = records.get(0);
// if the same institution tries to import an exam that already exists // if the same institution tries to import an exam that already exists throw an error
// open the existing. otherwise create new one if requested
if (exam.institutionId.equals(examRecord.getInstitutionId())) { if (exam.institutionId.equals(examRecord.getInstitutionId())) {
final ExamRecord newRecord = new ExamRecord( throw new DuplicateResourceException(EntityType.EXAM, exam.externalId);
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());
} }
} }

View file

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

View file

@ -476,12 +476,8 @@ public class MoodleCourseAccess extends AbstractCourseAccess {
final Map<String, CourseData> finalCourseDataRef = courseData; final Map<String, CourseData> finalCourseDataRef = courseData;
courseQuizData.quizzes courseQuizData.quizzes
.forEach(quiz -> { .stream()
final CourseData course = finalCourseDataRef.get(quiz.course); .forEach(quiz -> fillSelectedQuizzes(quizIds, finalCourseDataRef, quiz));
if (course != null) {
course.quizzes.add(quiz);
}
});
final String urlPrefix = (lmsSetup.lmsApiUrl.endsWith(Constants.URL_PATH_SEPARATOR)) final String urlPrefix = (lmsSetup.lmsApiUrl.endsWith(Constants.URL_PATH_SEPARATOR))
? lmsSetup.lmsApiUrl + MOODLE_QUIZ_START_URL_PATH ? 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( private Collection<CourseData> getCoursesForIds(
final MoodleAPIRestTemplate restTemplate, final MoodleAPIRestTemplate restTemplate,
final Set<String> ids) { final Set<String> ids) {

View file

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

View file

@ -133,7 +133,10 @@ public class StringConverter implements AttributeValueConverter {
// decrypt internally encrypted password and hash it for export // decrypt internally encrypted password and hash it for export
// NOTE: see special case description in ExamConfigXMLParser.createConfigurationValue // 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)) { if (plainText.endsWith(Constants.IMPORTED_PASSWORD_MARKER)) {
return plainText.replace(Constants.IMPORTED_PASSWORD_MARKER, StringUtils.EMPTY); return plainText.replace(Constants.IMPORTED_PASSWORD_MARKER, StringUtils.EMPTY);
} else { } 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 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 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. */ * @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 /** Get the exam identifier of the client connection of this ClientIndicator
* *

View file

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

View file

@ -81,6 +81,7 @@ public class ClientIndicatorFactory {
indicator.init( indicator.init(
indicatorDef, indicatorDef,
clientConnection.id, clientConnection.id,
clientConnection.status.clientActiveStatus,
this.enableCaching); this.enableCaching);
result.add(indicator); result.add(indicator);
@ -105,9 +106,11 @@ public class ClientIndicatorFactory {
null, null,
null, null,
Arrays.asList(new Indicator.Threshold(5000d, null, null))); Arrays.asList(new Indicator.Threshold(5000d, null, null)));
pingIndicator.init( pingIndicator.init(
indicator, indicator,
clientConnection.id, clientConnection.id,
clientConnection.status.clientActiveStatus,
this.enableCaching); this.enableCaching);
result.add(pingIndicator); 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 java.io.ByteArrayOutputStream;
import org.mybatis.dynamic.sql.SqlBuilder;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.Cacheable;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service; 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;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamStatus; 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.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.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result; 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.ClientConnectionDAO;
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.RemoteProctoringRoomDAO; 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_RUNNING_EXAM = "RUNNING_EXAM";
public static final String CACHE_NAME_ACTIVE_CLIENT_CONNECTION = "ACTIVE_CLIENT_CONNECTION"; 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_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); private static final Logger log = LoggerFactory.getLogger(ExamSessionCacheService.class);
@ -56,7 +48,6 @@ public class ExamSessionCacheService {
private final ClientConnectionDAO clientConnectionDAO; private final ClientConnectionDAO clientConnectionDAO;
private final InternalClientConnectionDataFactory internalClientConnectionDataFactory; private final InternalClientConnectionDataFactory internalClientConnectionDataFactory;
private final ExamConfigService sebExamConfigService; private final ExamConfigService sebExamConfigService;
private final ClientEventRecordMapper clientEventRecordMapper;
private final ExamUpdateHandler examUpdateHandler; private final ExamUpdateHandler examUpdateHandler;
protected ExamSessionCacheService( protected ExamSessionCacheService(
@ -64,7 +55,6 @@ public class ExamSessionCacheService {
final ClientConnectionDAO clientConnectionDAO, final ClientConnectionDAO clientConnectionDAO,
final InternalClientConnectionDataFactory internalClientConnectionDataFactory, final InternalClientConnectionDataFactory internalClientConnectionDataFactory,
final ExamConfigService sebExamConfigService, final ExamConfigService sebExamConfigService,
final ClientEventRecordMapper clientEventRecordMapper,
final ExamUpdateHandler examUpdateHandler, final ExamUpdateHandler examUpdateHandler,
final RemoteProctoringRoomDAO remoteProctoringRoomDAO) { final RemoteProctoringRoomDAO remoteProctoringRoomDAO) {
@ -72,7 +62,6 @@ public class ExamSessionCacheService {
this.clientConnectionDAO = clientConnectionDAO; this.clientConnectionDAO = clientConnectionDAO;
this.internalClientConnectionDataFactory = internalClientConnectionDataFactory; this.internalClientConnectionDataFactory = internalClientConnectionDataFactory;
this.sebExamConfigService = sebExamConfigService; this.sebExamConfigService = sebExamConfigService;
this.clientEventRecordMapper = clientEventRecordMapper;
this.examUpdateHandler = examUpdateHandler; this.examUpdateHandler = examUpdateHandler;
} }
@ -125,7 +114,7 @@ public class ExamSessionCacheService {
} }
public boolean isRunning(final Exam exam) { public boolean isRunning(final Exam exam) {
if (exam == null) { if (exam == null || !exam.active) {
return false; 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) { private ClientConnection getClientConnectionByToken(final String connectionToken) {
final Result<ClientConnection> result = this.clientConnectionDAO final Result<ClientConnection> result = this.clientConnectionDAO
.byConnectionToken(connectionToken); .byConnectionToken(connectionToken);
if (result.hasError()) { 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()); result.getError());
return null; return null;
} }

View file

@ -15,12 +15,11 @@ import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.NoSuchElementException; import java.util.NoSuchElementException;
import java.util.Objects; import java.util.Objects;
import java.util.function.Function; import java.util.Set;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.BooleanUtils;
import org.mybatis.dynamic.sql.SqlBuilder;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value; 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.model.session.ClientConnectionData;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result; 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.ClientConnectionDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamConfigurationMapDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamConfigurationMapDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO; 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.LmsAPIService;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionService; 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.ExamSessionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.indicator.IndicatorDistributedRequestCache;
@Lazy @Lazy
@Service @Service
@ -59,37 +54,33 @@ public class ExamSessionServiceImpl implements ExamSessionService {
private static final Logger log = LoggerFactory.getLogger(ExamSessionServiceImpl.class); private static final Logger log = LoggerFactory.getLogger(ExamSessionServiceImpl.class);
private final ClientConnectionDAO clientConnectionDAO; private final ClientConnectionDAO clientConnectionDAO;
private final ClientConnectionMinMapper clientConnectionMinMapper;
private final IndicatorDAO indicatorDAO; private final IndicatorDAO indicatorDAO;
private final ExamSessionCacheService examSessionCacheService; private final ExamSessionCacheService examSessionCacheService;
private final ExamDAO examDAO; private final ExamDAO examDAO;
private final ExamConfigurationMapDAO examConfigurationMapDAO; private final ExamConfigurationMapDAO examConfigurationMapDAO;
private final CacheManager cacheManager; private final CacheManager cacheManager;
private final SEBRestrictionService sebRestrictionService; private final SEBRestrictionService sebRestrictionService;
private final IndicatorDistributedRequestCache indicatorDistributedRequestCache;
private final boolean distributedSetup; private final boolean distributedSetup;
private long lastConnectionTokenCacheUpdate = 0;
protected ExamSessionServiceImpl( protected ExamSessionServiceImpl(
final ExamSessionCacheService examSessionCacheService, final ExamSessionCacheService examSessionCacheService,
final ClientConnectionMinMapper clientConnectionMinMapper,
final ExamDAO examDAO, final ExamDAO examDAO,
final ExamConfigurationMapDAO examConfigurationMapDAO, final ExamConfigurationMapDAO examConfigurationMapDAO,
final ClientConnectionDAO clientConnectionDAO, final ClientConnectionDAO clientConnectionDAO,
final IndicatorDAO indicatorDAO, final IndicatorDAO indicatorDAO,
final CacheManager cacheManager, final CacheManager cacheManager,
final SEBRestrictionService sebRestrictionService, final SEBRestrictionService sebRestrictionService,
final IndicatorDistributedRequestCache indicatorDistributedRequestCache,
@Value("${sebserver.webservice.distributed:false}") final boolean distributedSetup) { @Value("${sebserver.webservice.distributed:false}") final boolean distributedSetup) {
this.examSessionCacheService = examSessionCacheService; this.examSessionCacheService = examSessionCacheService;
this.clientConnectionMinMapper = clientConnectionMinMapper;
this.examDAO = examDAO; this.examDAO = examDAO;
this.examConfigurationMapDAO = examConfigurationMapDAO; this.examConfigurationMapDAO = examConfigurationMapDAO;
this.clientConnectionDAO = clientConnectionDAO; this.clientConnectionDAO = clientConnectionDAO;
this.cacheManager = cacheManager; this.cacheManager = cacheManager;
this.indicatorDAO = indicatorDAO; this.indicatorDAO = indicatorDAO;
this.sebRestrictionService = sebRestrictionService; this.sebRestrictionService = sebRestrictionService;
this.indicatorDistributedRequestCache = indicatorDistributedRequestCache;
this.distributedSetup = distributedSetup; this.distributedSetup = distributedSetup;
} }
@ -324,21 +315,11 @@ public class ExamSessionServiceImpl implements ExamSessionService {
final ClientConnectionDataInternal activeClientConnection = this.examSessionCacheService final ClientConnectionDataInternal activeClientConnection = this.examSessionCacheService
.getClientConnection(connectionToken); .getClientConnection(connectionToken);
if (activeClientConnection == null) { if (activeClientConnection == null) {
throw new NoSuchElementException("Client Connection with token: " + connectionToken); 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; return activeClientConnection;
}); });
@ -349,35 +330,20 @@ public class ExamSessionServiceImpl implements ExamSessionService {
final Long examId, final Long examId,
final Predicate<ClientConnectionData> filter) { 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 updateClientConnections(examId);
// 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()));
} else { return this.clientConnectionDAO
return Result.tryCatch(() -> this.clientConnectionDAO
.getConnectionTokens(examId) .getConnectionTokens(examId)
.getOrThrow() .getOrThrow()
.stream() .stream()
.map(this.examSessionCacheService::getClientConnection) .map(token -> getConnectionData(token).getOr(null))
.filter(Objects::nonNull)
.filter(filter) .filter(filter)
.collect(Collectors.toList())); .collect(Collectors.toList());
}
});
} }
@Override @Override
@ -415,32 +381,43 @@ public class ExamSessionServiceImpl implements ExamSessionService {
.forEach(token -> { .forEach(token -> {
// evict client connection // evict client connection
this.examSessionCacheService.evictClientConnection(token); this.examSessionCacheService.evictClientConnection(token);
// evict also cached ping record
this.examSessionCacheService.evictPingRecord(token);
}); });
return exam; return exam;
}); });
} }
private Function<ClientConnectionMinRecord, ClientConnectionDataInternal> distributedClientConnectionUpdateFunction( // If we are in a distributed setup the active connection token cache get flushed
final Predicate<ClientConnectionData> filter) { // 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 -> { try {
ClientConnectionDataInternal clientConnection = this.examSessionCacheService if (this.distributedSetup &&
.getClientConnection(cd.connection_token); System.currentTimeMillis() - this.lastConnectionTokenCacheUpdate > Constants.SECOND_IN_MILLIS) {
if (filter.test(clientConnection)) { // go trough all client connection and update the ones that not up to date
if (cd.update_time != null && this.clientConnectionDAO.evictConnectionTokenCache(examId);
!cd.update_time.equals(clientConnection.clientConnection.updateTime)) {
this.examSessionCacheService.evictClientConnection(cd.connection_token); final Set<Long> timestamps = this.clientConnectionDAO
clientConnection = this.examSessionCacheService .getConnectionTokens(examId)
.getClientConnection(cd.connection_token); .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.BooleanUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.cache.Cache; 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.model.session.ClientEvent.EventType;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result; 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.WebserviceInfo;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientConnectionRecord; import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientConnectionRecord;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientEventRecord; 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.exam.ExamAdminService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.EventHandlingStrategy; 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.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.SEBClientConnectionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientInstructionService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientInstructionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientNotificationService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientNotificationService;
@ -68,7 +66,6 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
private final CacheManager cacheManager; private final CacheManager cacheManager;
private final EventHandlingStrategy eventHandlingStrategy; private final EventHandlingStrategy eventHandlingStrategy;
private final ClientConnectionDAO clientConnectionDAO; private final ClientConnectionDAO clientConnectionDAO;
private final PingHandlingStrategy pingHandlingStrategy;
private final SEBClientConfigDAO sebClientConfigDAO; private final SEBClientConfigDAO sebClientConfigDAO;
private final SEBClientInstructionService sebInstructionService; private final SEBClientInstructionService sebInstructionService;
private final SEBClientNotificationService sebClientNotificationService; private final SEBClientNotificationService sebClientNotificationService;
@ -78,7 +75,7 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
protected SEBClientConnectionServiceImpl( protected SEBClientConnectionServiceImpl(
final ExamSessionService examSessionService, final ExamSessionService examSessionService,
final EventHandlingStrategyFactory eventHandlingStrategyFactory, final EventHandlingStrategyFactory eventHandlingStrategyFactory,
final PingHandlingStrategyFactory pingHandlingStrategyFactory,
final SEBClientConfigDAO sebClientConfigDAO, final SEBClientConfigDAO sebClientConfigDAO,
final SEBClientInstructionService sebInstructionService, final SEBClientInstructionService sebInstructionService,
final SEBClientNotificationService sebClientNotificationService, final SEBClientNotificationService sebClientNotificationService,
@ -88,7 +85,6 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
this.examSessionCacheService = examSessionService.getExamSessionCacheService(); this.examSessionCacheService = examSessionService.getExamSessionCacheService();
this.cacheManager = examSessionService.getCacheManager(); this.cacheManager = examSessionService.getCacheManager();
this.clientConnectionDAO = examSessionService.getClientConnectionDAO(); this.clientConnectionDAO = examSessionService.getClientConnectionDAO();
this.pingHandlingStrategy = pingHandlingStrategyFactory.get();
this.eventHandlingStrategy = eventHandlingStrategyFactory.get(); this.eventHandlingStrategy = eventHandlingStrategyFactory.get();
this.sebClientConfigDAO = sebClientConfigDAO; this.sebClientConfigDAO = sebClientConfigDAO;
this.sebInstructionService = sebInstructionService; this.sebInstructionService = sebInstructionService;
@ -368,11 +364,6 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
updatedClientConnection); updatedClientConnection);
} }
// notify ping handler about established connection
this.pingHandlingStrategy.initForConnection(
updatedClientConnection.id,
connectionToken);
return updatedClientConnection; return updatedClientConnection;
}); });
} }
@ -520,7 +511,8 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
try { try {
final Cache cache = this.cacheManager.getCache(ExamSessionCacheService.CACHE_NAME_ACTIVE_CLIENT_CONNECTION); 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 this.examSessionService
.getExamDAO() .getExamDAO()
.allRunningExamIds() .allRunningExamIds()
@ -538,8 +530,8 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
.map(token -> cache.get(token, ClientConnectionDataInternal.class)) .map(token -> cache.get(token, ClientConnectionDataInternal.class))
.filter(Objects::nonNull) .filter(Objects::nonNull)
.filter(connection -> connection.pingIndicator != null && .filter(connection -> connection.pingIndicator != null &&
connection.clientConnection.status.establishedStatus) connection.clientConnection.status.clientActiveStatus)
.forEach(missingPingUpdate(now)); .forEach(connection -> missingPingUpdate.accept(connection));
} catch (final Exception e) { } catch (final Exception e) {
log.error("Failed to update ping events: ", e); log.error("Failed to update ping events: ", e);
@ -557,7 +549,13 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
final long timestamp, final long timestamp,
final int pingNumber) { 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); return this.sebInstructionService.getInstructionJSON(connectionToken);
} }
@ -732,8 +730,6 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
private ClientConnectionDataInternal reloadConnectionCache(final String connectionToken) { private ClientConnectionDataInternal reloadConnectionCache(final String connectionToken) {
// evict cached ClientConnection // evict cached ClientConnection
this.examSessionCacheService.evictClientConnection(connectionToken); this.examSessionCacheService.evictClientConnection(connectionToken);
// evict also cached ping record
this.examSessionCacheService.evictPingRecord(connectionToken);
// and load updated ClientConnection into cache // and load updated ClientConnection into cache
return this.examSessionCacheService.getClientConnection(connectionToken); 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 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 int INSTRUCTION_QUEUE_MAX_SIZE = 10;
private static final String JSON_INST = "instruction"; private static final String JSON_INST = "instruction";
private static final String JSON_ATTR = "attributes"; 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 // Since the queue is empty check periodically if there are active instructions on the persistent storage
final long currentTimeMillis = System.currentTimeMillis(); final long currentTimeMillis = System.currentTimeMillis();
if (currentTimeMillis - this.lastRefresh > Constants.SECOND_IN_MILLIS) { if (currentTimeMillis - this.lastRefresh > PERSISTENT_UPDATE_INTERVAL) {
this.lastRefresh = currentTimeMillis; this.lastRefresh = currentTimeMillis;
loadInstructions(connectionToken) loadInstructions()
.onError(error -> log.error( .onError(error -> log.error(
"Failed load instructions from persistent storage and to refresh cache: ", "Failed load instructions from persistent storage and to refresh cache: ",
error)); 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, // 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 // for closed or disposed client connections and remove them from cache
synchronized (this.instructions) { synchronized (this.instructions) {
final Result<Collection<String>> result = this.clientConnectionDAO final Result<Collection<String>> result = this.clientConnectionDAO
.getInactiveConnctionTokens(this.instructions.keySet()); .getInactiveConnctionTokens(this.instructions.keySet());
if (result.hasValue()) { if (result.hasValue()) {
result.get().stream().forEach(token -> this.instructions.remove(token)); 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() { private Result<Void> loadInstructions() {
return Result.tryCatch(() -> this.clientInstructionDAO.getAllActive() return Result.tryCatch(() -> this.clientInstructionDAO.getAllActive()
.getOrThrow() .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; package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.indicator;
import org.joda.time.DateTime; import org.joda.time.DateTimeUtils;
import org.joda.time.DateTimeZone;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.model.exam.Indicator; import ch.ethz.seb.sebserver.gbl.model.exam.Indicator;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ClientIndicator; import ch.ethz.seb.sebserver.webservice.servicelayer.session.ClientIndicator;
public abstract class AbstractClientIndicator implements ClientIndicator { public abstract class AbstractClientIndicator implements ClientIndicator {
private static final long PERSISTENT_UPDATE_INTERVAL = Constants.SECOND_IN_MILLIS;
protected Long indicatorId; protected Long indicatorId;
protected Long examId; protected Long examId;
protected Long connectionId; protected Long connectionId;
protected boolean cachingEnabled; protected boolean cachingEnabled;
protected boolean active = true;
protected long lastPersistentUpdate = 0;
protected boolean valueInitializes = false; protected boolean valueInitializes = false;
protected double currentValue = Double.NaN; protected double currentValue = Double.NaN;
@ -28,11 +32,13 @@ public abstract class AbstractClientIndicator implements ClientIndicator {
public void init( public void init(
final Indicator indicatorDefinition, final Indicator indicatorDefinition,
final Long connectionId, final Long connectionId,
final boolean active,
final boolean cachingEnabled) { final boolean cachingEnabled) {
this.indicatorId = (indicatorDefinition != null) ? indicatorDefinition.id : -1; this.indicatorId = (indicatorDefinition != null) ? indicatorDefinition.id : -1;
this.examId = (indicatorDefinition != null) ? indicatorDefinition.examId : -1; this.examId = (indicatorDefinition != null) ? indicatorDefinition.examId : -1;
this.connectionId = connectionId; this.connectionId = connectionId;
this.active = active;
this.cachingEnabled = cachingEnabled; this.cachingEnabled = cachingEnabled;
} }
@ -58,11 +64,20 @@ public abstract class AbstractClientIndicator implements ClientIndicator {
@Override @Override
public double getValue() { public double getValue() {
if (!this.valueInitializes || !this.cachingEnabled) { final long now = DateTimeUtils.currentTimeMillis();
this.currentValue = computeValueAt(DateTime.now(DateTimeZone.UTC).getMillis()); if (!this.valueInitializes) {
this.currentValue = computeValueAt(now);
this.lastPersistentUpdate = now;
this.valueInitializes = true; 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; return this.currentValue;
} }

View file

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

View file

@ -56,14 +56,11 @@ public abstract class AbstractLogLevelCountIndicator extends AbstractLogIndicato
@Override @Override
public double computeValueAt(final long timestamp) { public double computeValueAt(final long timestamp) {
try { try {
// TODO to boost performance here within a distributed setup, invent a new cache for all log count values final Long errors = this.clientEventRecordMapper
// of the running exam. So all indicators get the values from cache and only one single SQL call .countByExample()
// 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()
.where(ClientEventRecordDynamicSqlSupport.clientConnectionId, isEqualTo(this.connectionId)) .where(ClientEventRecordDynamicSqlSupport.clientConnectionId, isEqualTo(this.connectionId))
.and(ClientEventRecordDynamicSqlSupport.type, isIn(this.eventTypeIds)) .and(ClientEventRecordDynamicSqlSupport.type, isIn(this.eventTypeIds))
.and(ClientEventRecordDynamicSqlSupport.serverTime, isLessThan(timestamp)) .and(ClientEventRecordDynamicSqlSupport.serverTime, isLessThan(timestamp))

View file

@ -64,11 +64,6 @@ public abstract class AbstractLogNumberIndicator extends AbstractLogIndicator {
public double computeValueAt(final long timestamp) { public double computeValueAt(final long timestamp) {
try { 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() final List<ClientEventRecord> execute = this.clientEventRecordMapper.selectByExample()
.where(ClientEventRecordDynamicSqlSupport.clientConnectionId, isEqualTo(this.connectionId)) .where(ClientEventRecordDynamicSqlSupport.clientConnectionId, isEqualTo(this.connectionId))
.and(ClientEventRecordDynamicSqlSupport.type, isIn(this.eventTypeIds)) .and(ClientEventRecordDynamicSqlSupport.type, isIn(this.eventTypeIds))

View file

@ -13,54 +13,61 @@ import java.util.EnumSet;
import java.util.Set; import java.util.Set;
import org.joda.time.DateTime; import org.joda.time.DateTime;
import org.joda.time.DateTimeUtils;
import org.joda.time.DateTimeZone; 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.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.datalayer.batis.model.ClientEventRecord;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientEventDAO;
public abstract class AbstractPingIndicator extends AbstractClientIndicator { 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)); private final Set<EventType> EMPTY_SET = Collections.unmodifiableSet(EnumSet.noneOf(EventType.class));
protected final ClientEventExtensionMapper clientEventExtensionMapper; protected final ClientEventDAO clientEventDAO;
protected final IndicatorDistributedRequestCache indicatorDistributedRequestCache;
protected long pingLatency; private long lastUpdate = 0;
protected int pingCount = 0; protected ClientEventRecord pingRecord = null;
protected int pingNumber = 0;
protected AbstractPingIndicator(
final ClientEventExtensionMapper clientEventExtensionMapper,
final IndicatorDistributedRequestCache indicatorDistributedRequestCache) {
protected AbstractPingIndicator(final ClientEventDAO clientEventDAO) {
super(); super();
this.clientEventExtensionMapper = clientEventExtensionMapper; this.clientEventDAO = clientEventDAO;
this.indicatorDistributedRequestCache = indicatorDistributedRequestCache; }
@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) { public final void notifyPing(final long timestamp, final int pingNumber) {
final long now = DateTime.now(DateTimeZone.UTC).getMillis(); final long now = DateTime.now(DateTimeZone.UTC).getMillis();
this.pingLatency = now - timestamp;
super.currentValue = now; super.currentValue = now;
this.pingCount++; super.lastPersistentUpdate = now;
this.pingNumber = pingNumber;
}
@Override if (!this.cachingEnabled && this.pingRecord != null) {
public final double computeValueAt(final long timestamp) {
if (this.cachingEnabled) {
return timestamp;
} else {
try {
return this.indicatorDistributedRequestCache
.getPingTimes(this.examId)
.getOrDefault(this.connectionId, 0L);
} catch (final Exception e) { // Update last ping time on persistent storage
return Double.NaN; 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; 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); public abstract ClientEventRecord updateLogEvent(final long now);
} }

View file

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

View file

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

View file

@ -280,25 +280,11 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
proctoringRoom.id) proctoringRoom.id)
.getOrThrow(); .getOrThrow();
final Result<RemoteProctoringRoom> townhallRoomResult = this.remoteProctoringRoomDAO applyProcotringInstruction(
.getTownhallRoom(cc.getExamId()); 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) { } catch (final Exception e) {
log.error("Failed to assign connection to collecting room: {}", cc, 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( private Result<Void> applyProcotringInstruction(
final Long examId, final Long examId,
final String connectionToken, final String connectionToken) {
final String roomName,
final String subject) {
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
final ProctoringServiceSettings settings = this.examAdminService final ProctoringServiceSettings settings = this.examAdminService
@ -614,10 +598,32 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
.getExamProctoringService(settings.serverType) .getExamProctoringService(settings.serverType)
.getOrThrow(); .getOrThrow();
sendJoinCollectingRoomInstructions( final Result<RemoteProctoringRoom> townhallRoomResult = this.remoteProctoringRoomDAO
settings, .getTownhallRoom(examId);
Arrays.asList(connectionToken),
examProctoringService); 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); final String instructionConfirm = request.getParameter(API.EXAM_API_PING_INSTRUCTION_CONFIRM);
if (log.isTraceEnabled()) { if (log.isTraceEnabled()) {
log.trace("****************** SEB client connection: {}", connectionToken); log.trace("****************** SEB client connection: {} ip: ",
connectionToken,
getClientAddress(request));
} }
if (instructionConfirm != null) { if (instructionConfirm != null) {
@ -352,32 +354,6 @@ public class ExamAPI_V1_Controller {
final ServletOutputStream outputStream = response.getOutputStream(); 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 { try {
this.examSessionService this.examSessionService

View file

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

View file

@ -11,7 +11,7 @@ logging.level.ch=INFO
logging.level.org.springframework.cache=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.lms.impl=DEBUG
logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.session=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.connect-timeout=150000
sebserver.http.client.connection-request-timeout=100000 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.enabled=true
spring.flyway.locations=classpath:config/sql/base spring.flyway.locations=classpath:config/sql/base
spring.flyway.cleanDisabled=true spring.flyway.cleanDisabled=true
spring.flyway.ignoreIgnoredMigrations=true
spring.datasource.driver-class-name=org.mariadb.jdbc.Driver spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
spring.datasource.hikari.initializationFailTimeout=3000 spring.datasource.hikari.initializationFailTimeout=3000
spring.datasource.hikari.connectionTimeout=30000 spring.datasource.hikari.connectionTimeout=30000

View file

@ -82,17 +82,6 @@
</resources> </resources>
</cache> </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"> <cache alias="QUIZ_DATA_CACHE">
<key-type>java.lang.String</key-type> <key-type>java.lang.String</key-type>
<value-type>ch.ethz.seb.sebserver.gbl.model.exam.QuizData</value-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.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.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.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.title=LMS Exam Details
sebserver.quizdiscovery.quiz.details.institution=Institution 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.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.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.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.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. 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.proctoringDetectTalkingDisplay.tooltips=
sebserver.examconfig.props.label.remoteProctoringViewShow=Proctoring View Display Policy 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=Never
sebserver.examconfig.props.label.remoteProctoringViewShow.0.tooltip= sebserver.examconfig.props.label.remoteProctoringViewShow.0.tooltip=
sebserver.examconfig.props.label.remoteProctoringViewShow.1=Allow to Show 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=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.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=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=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=Enable Zoom
sebserver.examconfig.props.label.zoomEnable.tooltip= sebserver.examconfig.props.label.zoomEnable.tooltip=
sebserver.examconfig.props.label.zoomFeatureFlagChat=Enable Chat 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=Enable Close Captions
sebserver.examconfig.props.label.zoomFeatureFlagCloseCaptions.tooltip= sebserver.examconfig.props.label.zoomFeatureFlagCloseCaptions.tooltip=Not yet implemented
sebserver.examconfig.props.label.zoomFeatureFlagDisplayingMeetingName=Display Meeting Name sebserver.examconfig.props.label.zoomFeatureFlagDisplayMeetingName=Display Meeting Name
sebserver.examconfig.props.label.zoomFeatureFlagDisplayMeetingName.tooltip sebserver.examconfig.props.label.zoomFeatureFlagDisplayMeetingName.tooltip=Not yet implemented
sebserver.examconfig.props.label.zoomFeatureFlagRaiseHand=Enable Raise Hand 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=Allow Recording
sebserver.examconfig.props.label.zoomFeatureFlagRecording.tooltip= sebserver.examconfig.props.label.zoomFeatureFlagRecording.tooltip=
sebserver.examconfig.props.label.zoomFeatureFlagTileView=Allow Tile View 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=Room
sebserver.examconfig.props.label.zoomRoom.tooltip= sebserver.examconfig.props.label.zoomRoom.tooltip=
sebserver.examconfig.props.label.zoomServerURL=Server URL 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=Info Mail
sebserver.examconfig.props.label.zoomUserInfoEMail.tooltip= sebserver.examconfig.props.label.zoomUserInfoEMail.tooltip=
sebserver.examconfig.props.label.zoomVideoMuted=Video Initially Muted 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=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=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=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=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 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.datalayer.batis.model.ClientEventRecord;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.ClientConnectionDataInternal; 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.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" }) @Sql(scripts = { "classpath:schema-test.sql", "classpath:data-test.sql", "classpath:data-test-additional.sql" })
public class SebConnectionTest extends ExamAPIIntegrationTester { public class SebConnectionTest extends ExamAPIIntegrationTester {
@ -468,20 +467,15 @@ public class SebConnectionTest extends ExamAPIIntegrationTester {
assertFalse(ccdi.indicatorValues.isEmpty()); assertFalse(ccdi.indicatorValues.isEmpty());
final IndicatorValue pingIndicator = ccdi.indicatorValues.iterator().next(); final IndicatorValue pingIndicator = ccdi.indicatorValues.iterator().next();
assertTrue(pingIndicator.getType() == IndicatorType.LAST_PING); assertTrue(pingIndicator.getType() == IndicatorType.LAST_PING);
assertEquals("0", String.valueOf(((AbstractPingIndicator) pingIndicator).getPingNumber()));
super.sendPing(accessToken, connectionToken, 1); super.sendPing(accessToken, connectionToken, 1);
Thread.sleep(200); Thread.sleep(200);
assertEquals("1", String.valueOf(((AbstractPingIndicator) pingIndicator).getPingNumber()));
super.sendPing(accessToken, connectionToken, 2); super.sendPing(accessToken, connectionToken, 2);
Thread.sleep(200); Thread.sleep(200);
assertEquals("2", String.valueOf(((AbstractPingIndicator) pingIndicator).getPingNumber()));
super.sendPing(accessToken, connectionToken, 3); super.sendPing(accessToken, connectionToken, 3);
Thread.sleep(200); Thread.sleep(200);
assertEquals("3", String.valueOf(((AbstractPingIndicator) pingIndicator).getPingNumber()));
super.sendPing(accessToken, connectionToken, 5); super.sendPing(accessToken, connectionToken, 5);
Thread.sleep(200); Thread.sleep(200);
assertEquals("5", String.valueOf(((AbstractPingIndicator) pingIndicator).getPingNumber()));
} }
@Test @Test
@ -531,7 +525,6 @@ public class SebConnectionTest extends ExamAPIIntegrationTester {
assertFalse(ccdi.indicatorValues.isEmpty()); assertFalse(ccdi.indicatorValues.isEmpty());
final IndicatorValue pingIndicator = ccdi.indicatorValues.iterator().next(); final IndicatorValue pingIndicator = ccdi.indicatorValues.iterator().next();
assertTrue(pingIndicator.getType() == IndicatorType.LAST_PING); assertTrue(pingIndicator.getType() == IndicatorType.LAST_PING);
assertEquals("0", String.valueOf(((AbstractPingIndicator) pingIndicator).getPingNumber()));
MockHttpServletResponse sendEvent = super.sendEvent( MockHttpServletResponse sendEvent = super.sendEvent(
accessToken, accessToken,

View file

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