Merge remote-tracking branch 'origin/dev-1.2' into development
This commit is contained in:
commit
bc9b3de297
50 changed files with 526 additions and 632 deletions
14
.github/workflows/buildReporting.yml
vendored
14
.github/workflows/buildReporting.yml
vendored
|
@ -59,6 +59,14 @@ jobs:
|
|||
run: echo $SHA
|
||||
env:
|
||||
SHA: ${{ steps.short-sha.outputs.sha }}
|
||||
-
|
||||
name: Set env
|
||||
run: echo "TAG_NAME=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||
-
|
||||
name: Test tag name
|
||||
run: |
|
||||
echo $TAG_NAME
|
||||
echo ${{ env.TAG_NAME }}
|
||||
-
|
||||
name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
@ -77,12 +85,12 @@ jobs:
|
|||
restore-keys: ${{ runner.os }}-m2
|
||||
-
|
||||
name: Build with Maven
|
||||
run: mvn clean install -Dmaven.test.skip=true -Dsebserver-version="${{ env.SHA }}"
|
||||
run: mvn clean install -Dmaven.test.skip=true -Dsebserver-version="${{ env.TAG_NAME }}"
|
||||
env:
|
||||
sebserver-version: ${{ env.SHA }}
|
||||
sebserver-version: ${{ env.TAG_NAME }}
|
||||
-
|
||||
name: Simplify package name
|
||||
run: mv target/seb-server-${{ env.SHA }}.jar target/seb-server.jar
|
||||
run: mv target/seb-server-${{ env.TAG_NAME }}.jar target/seb-server.jar
|
||||
-
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
|
|
|
@ -37,7 +37,7 @@ import ch.ethz.seb.sebserver.gbl.profile.ProdWebServiceProfile;
|
|||
* SEB-Server uses Spring's profiles to consequently separate sub-components of the webservice
|
||||
* and GUI and can be used to start the components on separate servers or within the same
|
||||
* server instance. Additional to the usual profiles like dev, prod, test there are combining
|
||||
* profiles like dev-ws, dev-gui and prod-ws, prod-gui */
|
||||
* profiles like dev-ws, dev-gui and prod-ws, prod-gui test */
|
||||
@SpringBootApplication(exclude = {
|
||||
UserDetailsServiceAutoConfiguration.class,
|
||||
})
|
||||
|
|
|
@ -383,7 +383,6 @@ public final class Result<T> {
|
|||
@Override
|
||||
public String toString() {
|
||||
throw new RuntimeException("Result.toString is probably called by mistake !!!");
|
||||
//return "Result [value=" + this.value + ", error=" + this.error + "]";
|
||||
}
|
||||
|
||||
public interface TryCatchSupplier<T> {
|
||||
|
|
|
@ -92,7 +92,7 @@ public class ExamFormIndicators implements TemplateComposer {
|
|||
entityKey.modelId))
|
||||
.withEmptyMessage(INDICATOR_EMPTY_LIST_MESSAGE)
|
||||
.withMarkup()
|
||||
.withPaging(5)
|
||||
.withPaging(100)
|
||||
.hideNavigation()
|
||||
.withColumn(new ColumnDefinition<>(
|
||||
Domain.INDICATOR.ATTR_NAME,
|
||||
|
|
|
@ -12,6 +12,7 @@ import java.util.Arrays;
|
|||
import java.util.Collection;
|
||||
import java.util.function.BooleanSupplier;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.eclipse.swt.widgets.Composite;
|
||||
import org.joda.time.DateTime;
|
||||
|
@ -100,6 +101,8 @@ public class QuizLookupList implements TemplateComposer {
|
|||
new LocTextKey("sebserver.quizdiscovery.quiz.import.out.dated");
|
||||
private final static LocTextKey TEXT_KEY_CONFIRM_EXISTING =
|
||||
new LocTextKey("sebserver.quizdiscovery.quiz.import.existing.confirm");
|
||||
private final static LocTextKey TEXT_KEY_EXISTING =
|
||||
new LocTextKey("sebserver.quizdiscovery.quiz.import.existing");
|
||||
|
||||
private final static String TEXT_KEY_ADDITIONAL_ATTR_PREFIX =
|
||||
"sebserver.quizdiscovery.quiz.details.additional.";
|
||||
|
@ -147,6 +150,7 @@ public class QuizLookupList implements TemplateComposer {
|
|||
final CurrentUser currentUser = this.resourceService.getCurrentUser();
|
||||
final RestService restService = this.resourceService.getRestService();
|
||||
final I18nSupport i18nSupport = this.resourceService.getI18nSupport();
|
||||
final Long institutionId = currentUser.get().institutionId;
|
||||
|
||||
// content page layout with title
|
||||
final Composite content = this.widgetFactory.defaultPageLayout(
|
||||
|
@ -242,10 +246,10 @@ public class QuizLookupList implements TemplateComposer {
|
|||
.publish(false)
|
||||
|
||||
.newAction(ActionDefinition.QUIZ_DISCOVERY_EXAM_IMPORT)
|
||||
.withConfirm(importQuizConfirm(table, restService))
|
||||
.withConfirm(importQuizConfirm(institutionId, table, restService))
|
||||
.withSelect(
|
||||
table.getGrantedSelection(currentUser, NO_MODIFY_PRIVILEGE_ON_OTHER_INSTITUTION),
|
||||
action -> this.importQuizData(action, table),
|
||||
action -> this.importQuizData(institutionId, action, table, restService),
|
||||
EMPTY_SELECTION_TEXT)
|
||||
.publishIf(() -> examGrant.im(), false);
|
||||
}
|
||||
|
@ -257,6 +261,7 @@ public class QuizLookupList implements TemplateComposer {
|
|||
}
|
||||
|
||||
private Function<PageAction, LocTextKey> importQuizConfirm(
|
||||
final Long institutionId,
|
||||
final EntityTable<QuizData> table,
|
||||
final RestService restService) {
|
||||
|
||||
|
@ -264,12 +269,15 @@ public class QuizLookupList implements TemplateComposer {
|
|||
action.getSingleSelection();
|
||||
final QuizData selectedROWData = table.getSingleSelectedROWData();
|
||||
|
||||
final Collection<EntityKey> existingImports = restService.getBuilder(CheckExamImported.class)
|
||||
final Collection<Long> existingImports = restService.getBuilder(CheckExamImported.class)
|
||||
.withURIVariable(API.PARAM_MODEL_ID, selectedROWData.id)
|
||||
.call()
|
||||
.getOrThrow();
|
||||
.getOrThrow()
|
||||
.stream()
|
||||
.map(key -> Long.valueOf(key.modelId))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (existingImports != null && !existingImports.isEmpty()) {
|
||||
if (existingImports != null && !existingImports.isEmpty() && !existingImports.contains(institutionId)) {
|
||||
return TEXT_KEY_CONFIRM_EXISTING;
|
||||
} else {
|
||||
return null;
|
||||
|
@ -278,8 +286,10 @@ public class QuizLookupList implements TemplateComposer {
|
|||
}
|
||||
|
||||
private PageAction importQuizData(
|
||||
final Long institutionId,
|
||||
final PageAction action,
|
||||
final EntityTable<QuizData> table) {
|
||||
final EntityTable<QuizData> table,
|
||||
final RestService restService) {
|
||||
|
||||
action.getSingleSelection();
|
||||
final QuizData selectedROWData = table.getSingleSelectedROWData();
|
||||
|
@ -291,6 +301,18 @@ public class QuizLookupList implements TemplateComposer {
|
|||
}
|
||||
}
|
||||
|
||||
final Collection<Long> existingImports = restService.getBuilder(CheckExamImported.class)
|
||||
.withURIVariable(API.PARAM_MODEL_ID, selectedROWData.id)
|
||||
.call()
|
||||
.getOrThrow()
|
||||
.stream()
|
||||
.map(key -> Long.valueOf(key.modelId))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (existingImports.contains(institutionId)) {
|
||||
throw new PageMessageException(TEXT_KEY_EXISTING);
|
||||
}
|
||||
|
||||
return action
|
||||
.withEntityKey(action.getSingleSelection())
|
||||
.withParentEntityKey(new EntityKey(selectedROWData.lmsSetupId, EntityType.LMS_SETUP))
|
||||
|
|
|
@ -232,7 +232,7 @@ public class UserAccountList implements TemplateComposer {
|
|||
.newAction(ActionDefinition.USER_ACCOUNT_TOGGLE_ACTIVITY)
|
||||
.withExec(this.pageService.activationToggleActionFunction(table, EMPTY_SELECTION_TEXT_KEY))
|
||||
.withConfirm(this.pageService.confirmDeactivation(table))
|
||||
.publishIf(() -> userGrant.m(), false);
|
||||
.publishIf(() -> userGrant.im(), false);
|
||||
}
|
||||
|
||||
private PageAction editAction(final PageAction pageAction) {
|
||||
|
|
|
@ -169,7 +169,10 @@ public class PasswordFieldBuilder implements InputFieldBuilder {
|
|||
public String getValue() {
|
||||
final CharSequence pwd = this.control.getValue();
|
||||
if (StringUtils.isNotBlank(pwd)) {
|
||||
return this.cryptor.encrypt(pwd).toString();
|
||||
return this.cryptor
|
||||
.encrypt(pwd)
|
||||
.getOrThrow()
|
||||
.toString();
|
||||
}
|
||||
|
||||
return StringUtils.EMPTY;
|
||||
|
|
|
@ -11,6 +11,7 @@ package ch.ethz.seb.sebserver.gui.service.push;
|
|||
import java.util.function.Consumer;
|
||||
|
||||
import org.eclipse.rap.rwt.service.ServerPushSession;
|
||||
import org.eclipse.swt.SWTException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
|
@ -70,6 +71,8 @@ public class ServerPushService {
|
|||
business.accept(context);
|
||||
doUpdate(context, update);
|
||||
|
||||
} catch (final SWTException swte) {
|
||||
log.error("Disposed GUI widget(s) while update: {}", swte.getMessage());
|
||||
} catch (final Exception e) {
|
||||
log.error("Unexpected error while do business for server push service", e);
|
||||
context.internalStop = context.errorHandler.apply(e);
|
||||
|
|
|
@ -28,7 +28,7 @@ public class DeleteUserAccount extends RestCall<EntityProcessingReport> {
|
|||
|
||||
public DeleteUserAccount() {
|
||||
super(new TypeKey<>(
|
||||
CallType.ACTIVATION_DEACTIVATE,
|
||||
CallType.DELETE,
|
||||
EntityType.USER,
|
||||
new TypeReference<EntityProcessingReport>() {
|
||||
}),
|
||||
|
|
|
@ -195,10 +195,6 @@ public final class ClientConnectionTable {
|
|||
this.table.layout();
|
||||
}
|
||||
|
||||
// public int getUpdateErrors() {
|
||||
// return this.updateErrors;
|
||||
// }
|
||||
|
||||
public WidgetFactory getWidgetFactory() {
|
||||
return this.pageService.getWidgetFactory();
|
||||
}
|
||||
|
@ -337,6 +333,7 @@ public final class ClientConnectionTable {
|
|||
|
||||
try {
|
||||
|
||||
// TODO forceUpdateAll doeasn't work on distributed
|
||||
if (this.statusFilterChanged || this.forceUpdateAll || needsSync) {
|
||||
this.toDelete.clear();
|
||||
this.toDelete.addAll(this.tableMapping.keySet());
|
||||
|
|
|
@ -33,7 +33,7 @@ public interface ClientEventLastPingMapper {
|
|||
@SelectProvider(type = SqlProviderAdapter.class, method = "select")
|
||||
@ResultType(ClientEventLastPingRecord.class)
|
||||
@ConstructorArgs({
|
||||
@Arg(column = "client_connection_id", javaType = Long.class, jdbcType = JdbcType.BIGINT),
|
||||
@Arg(column = "id", javaType = Long.class, jdbcType = JdbcType.BIGINT),
|
||||
@Arg(column = "server_time", javaType = Long.class, jdbcType = JdbcType.BIGINT),
|
||||
})
|
||||
Collection<ClientEventLastPingRecord> selectMany(SelectStatementProvider select);
|
||||
|
@ -42,7 +42,7 @@ public interface ClientEventLastPingMapper {
|
|||
return SelectDSL.selectWithMapper(
|
||||
this::selectMany,
|
||||
|
||||
ClientEventRecordDynamicSqlSupport.clientConnectionId.as("client_connection_id"),
|
||||
ClientEventRecordDynamicSqlSupport.clientConnectionId.as("id"),
|
||||
ClientEventRecordDynamicSqlSupport.serverTime.as("server_time"))
|
||||
|
||||
.from(ClientEventRecordDynamicSqlSupport.clientEventRecord);
|
||||
|
@ -50,14 +50,14 @@ public interface ClientEventLastPingMapper {
|
|||
|
||||
final class ClientEventLastPingRecord {
|
||||
|
||||
public final Long connectionId;
|
||||
public final Long id;
|
||||
public final Long lastPingTime;
|
||||
|
||||
public ClientEventLastPingRecord(
|
||||
final Long client_connection_id,
|
||||
final Long id,
|
||||
final Long server_time) {
|
||||
|
||||
this.connectionId = client_connection_id;
|
||||
this.id = id;
|
||||
this.lastPingTime = server_time;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,6 +42,11 @@ public interface ClientConnectionDAO extends
|
|||
unless = "#result.hasError()")
|
||||
Result<Collection<String>> getConnectionTokens(Long examId);
|
||||
|
||||
@CacheEvict(cacheNames = CONNECTION_TOKENS_CACHE, key = "#examId")
|
||||
default void evictConnectionTokenCache(final Long examId) {
|
||||
|
||||
}
|
||||
|
||||
/** Get a list of all connection tokens of all connections (no matter what state)
|
||||
* of an exam.
|
||||
*
|
||||
|
@ -151,6 +156,8 @@ public interface ClientConnectionDAO extends
|
|||
* @return Result refer to true if the given ClientConnection is up to date */
|
||||
Result<Boolean> isUpToDate(ClientConnection clientConnection);
|
||||
|
||||
Result<Set<String>> getClientConnectionsOutOfSyc(Long examId, Set<Long> timestamps);
|
||||
|
||||
/** Indicates if the client connection for given exam and connection token is
|
||||
* in a ready state to send instructions.
|
||||
*
|
||||
|
|
|
@ -17,6 +17,7 @@ import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
|
|||
import ch.ethz.seb.sebserver.gbl.model.session.ClientNotification;
|
||||
import ch.ethz.seb.sebserver.gbl.model.session.ExtendedClientEvent;
|
||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientEventRecord;
|
||||
|
||||
public interface ClientEventDAO extends EntityDAO<ClientEvent, ClientEvent> {
|
||||
|
||||
|
@ -54,4 +55,10 @@ public interface ClientEventDAO extends EntityDAO<ClientEvent, ClientEvent> {
|
|||
* @return Result refer to the confirmed notification or to en error when happened */
|
||||
Result<ClientNotification> confirmPendingNotification(Long notificationId, Long clientConnectionId);
|
||||
|
||||
Result<ClientEventRecord> initPingEvent(Long connectionId);
|
||||
|
||||
void updatePingEvent(ClientEventRecord pingRecord);
|
||||
|
||||
Result<Long> getLastPing(Long pk);
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -44,7 +44,11 @@ public interface ExamDAO extends ActivatableEntityDAO<Exam, Exam>, BulkActionSup
|
|||
* happened */
|
||||
Result<Collection<Long>> allIdsOfInstitution(Long institutionId);
|
||||
|
||||
Result<Collection<Long>> allByQuizId(String quizId);
|
||||
/** Get all institution ids for that a specified exam for given quiz id already exists
|
||||
*
|
||||
* @param quizId The quiz or external identifier of the exam (LMS)
|
||||
* @return Result refer to a collection of primary keys of the institutions or to an error when happened */
|
||||
Result<Collection<Long>> allInstitutionIdsByQuizId(String quizId);
|
||||
|
||||
/** Updates the exam status for specified exam
|
||||
*
|
||||
|
|
|
@ -13,6 +13,7 @@ import static org.mybatis.dynamic.sql.SqlBuilder.*;
|
|||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
|
@ -553,9 +554,10 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO {
|
|||
}
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public Result<Boolean> isUpToDate(final ClientConnection clientConnection) {
|
||||
return Result.tryCatch(() -> this.clientConnectionRecordMapper
|
||||
.selectByExample()
|
||||
.countByExample()
|
||||
.where(
|
||||
ClientConnectionRecordDynamicSqlSupport.connectionToken,
|
||||
SqlBuilder.isEqualTo(clientConnection.connectionToken))
|
||||
|
@ -563,10 +565,31 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO {
|
|||
ClientConnectionRecordDynamicSqlSupport.updateTime,
|
||||
SqlBuilder.isEqualTo(clientConnection.updateTime))
|
||||
.build()
|
||||
.execute()
|
||||
.stream()
|
||||
.findFirst()
|
||||
.isPresent());
|
||||
.execute() > 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public Result<Set<String>> getClientConnectionsOutOfSyc(final Long examId, final Set<Long> timestamps) {
|
||||
return Result.tryCatch(() -> {
|
||||
final Set<String> result = new HashSet<>();
|
||||
|
||||
this.clientConnectionRecordMapper
|
||||
.selectByExample()
|
||||
.where(
|
||||
ClientConnectionRecordDynamicSqlSupport.examId,
|
||||
SqlBuilder.isEqualTo(examId))
|
||||
.build()
|
||||
.execute()
|
||||
.stream()
|
||||
.forEach(cc -> {
|
||||
if (!timestamps.contains(cc.getUpdateTime())) {
|
||||
result.add(cc.getConnectionToken());
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -14,7 +14,6 @@ import java.math.BigDecimal;
|
|||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
@ -243,7 +242,8 @@ public class ClientEventDAOImpl implements ClientEventDAO {
|
|||
public Result<ClientNotification> confirmPendingNotification(final Long notificationId,
|
||||
final Long clientConnectionId) {
|
||||
return Result.tryCatch(() -> {
|
||||
final Long pk = this.clientEventRecordMapper.selectIdsByExample()
|
||||
final Long pk = this.clientEventRecordMapper
|
||||
.selectIdsByExample()
|
||||
.where(ClientEventRecordDynamicSqlSupport.id, isEqualTo(notificationId))
|
||||
.and(ClientEventRecordDynamicSqlSupport.type, isEqualTo(EventType.NOTIFICATION.id))
|
||||
.build()
|
||||
|
@ -295,23 +295,99 @@ public class ClientEventDAOImpl implements ClientEventDAO {
|
|||
@Transactional
|
||||
public Result<Collection<EntityKey>> delete(final Set<EntityKey> all) {
|
||||
return Result.tryCatch(() -> {
|
||||
return all
|
||||
|
||||
final List<Long> pks = all
|
||||
.stream()
|
||||
.map(EntityKey::getModelId)
|
||||
.map(Long::parseLong)
|
||||
.map(pk -> {
|
||||
final int deleted = this.clientEventRecordMapper.deleteByPrimaryKey(pk);
|
||||
if (deleted == 1) {
|
||||
return new EntityKey(String.valueOf(pk), EntityType.CLIENT_EVENT);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
this.clientEventRecordMapper
|
||||
.deleteByExample()
|
||||
.where(ClientEventRecordDynamicSqlSupport.id, isIn(pks))
|
||||
.build()
|
||||
.execute();
|
||||
|
||||
return pks
|
||||
.stream()
|
||||
.map(pk -> new EntityKey(String.valueOf(pk), EntityType.CLIENT_EVENT))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// return all
|
||||
// .stream()
|
||||
// .map(EntityKey::getModelId)
|
||||
// .map(Long::parseLong)
|
||||
// .map(pk -> {
|
||||
// final int deleted = this.clientEventRecordMapper.deleteByPrimaryKey(pk);
|
||||
// if (deleted == 1) {
|
||||
// return new EntityKey(String.valueOf(pk), EntityType.CLIENT_EVENT);
|
||||
// } else {
|
||||
// return null;
|
||||
// }
|
||||
// })
|
||||
// .filter(Objects::nonNull)
|
||||
// .collect(Collectors.toList());
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public Result<ClientEventRecord> initPingEvent(final Long connectionId) {
|
||||
return Result.tryCatch(() -> {
|
||||
final List<ClientEventRecord> lastPingRec = this.clientEventRecordMapper
|
||||
.selectByExample()
|
||||
.where(ClientEventRecordDynamicSqlSupport.clientConnectionId, isEqualTo(connectionId))
|
||||
.and(ClientEventRecordDynamicSqlSupport.type, isEqualTo(EventType.LAST_PING.id))
|
||||
.build()
|
||||
.execute();
|
||||
|
||||
if (lastPingRec != null && !lastPingRec.isEmpty()) {
|
||||
return lastPingRec.get(0);
|
||||
}
|
||||
|
||||
final long millisecondsNow = Utils.getMillisecondsNow();
|
||||
final ClientEventRecord clientEventRecord = new ClientEventRecord();
|
||||
clientEventRecord.setClientConnectionId(connectionId);
|
||||
clientEventRecord.setType(EventType.LAST_PING.id);
|
||||
clientEventRecord.setClientTime(millisecondsNow);
|
||||
clientEventRecord.setServerTime(millisecondsNow);
|
||||
this.clientEventRecordMapper.insert(clientEventRecord);
|
||||
|
||||
try {
|
||||
|
||||
return this.clientEventRecordMapper
|
||||
.selectByExample()
|
||||
.where(ClientEventRecordDynamicSqlSupport.clientConnectionId, isEqualTo(connectionId))
|
||||
.and(ClientEventRecordDynamicSqlSupport.type, isEqualTo(EventType.LAST_PING.id))
|
||||
.build()
|
||||
.execute()
|
||||
.get(0);
|
||||
|
||||
} catch (final Exception e) {
|
||||
return clientEventRecord;
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void updatePingEvent(final ClientEventRecord pingRecord) {
|
||||
try {
|
||||
this.clientEventRecordMapper.updateByPrimaryKeySelective(pingRecord);
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to update ping event: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public Result<Long> getLastPing(final Long pk) {
|
||||
return Result.tryCatch(() -> this.clientEventRecordMapper
|
||||
.selectByPrimaryKey(pk)
|
||||
.getServerTime());
|
||||
}
|
||||
|
||||
private Result<ClientEventRecord> recordById(final Long id) {
|
||||
return Result.tryCatch(() -> {
|
||||
|
||||
|
|
|
@ -59,6 +59,7 @@ import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.LmsSetupRecordDyn
|
|||
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.AdditionalAttributeRecord;
|
||||
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ExamRecord;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.impl.BulkAction;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.DuplicateResourceException;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ResourceNotFoundException;
|
||||
|
@ -139,7 +140,7 @@ public class ExamDAOImpl implements ExamDAO {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Result<Collection<Long>> allByQuizId(final String quizId) {
|
||||
public Result<Collection<Long>> allInstitutionIdsByQuizId(final String quizId) {
|
||||
return Result.tryCatch(() -> {
|
||||
return this.examRecordMapper.selectByExample()
|
||||
.where(
|
||||
|
@ -151,7 +152,7 @@ public class ExamDAOImpl implements ExamDAO {
|
|||
.build()
|
||||
.execute()
|
||||
.stream()
|
||||
.map(rec -> rec.getId())
|
||||
.map(rec -> rec.getInstitutionId())
|
||||
.collect(Collectors.toList());
|
||||
});
|
||||
}
|
||||
|
@ -331,23 +332,9 @@ public class ExamDAOImpl implements ExamDAO {
|
|||
// used to save instead of create a new one
|
||||
if (records != null && records.size() > 0) {
|
||||
final ExamRecord examRecord = records.get(0);
|
||||
// if the same institution tries to import an exam that already exists
|
||||
// open the existing. otherwise create new one if requested
|
||||
// if the same institution tries to import an exam that already exists throw an error
|
||||
if (exam.institutionId.equals(examRecord.getInstitutionId())) {
|
||||
final ExamRecord newRecord = new ExamRecord(
|
||||
examRecord.getId(),
|
||||
null, null, null, null, null,
|
||||
(exam.type != null) ? exam.type.name() : ExamType.UNDEFINED.name(),
|
||||
null, // quitPassword
|
||||
null, // browser keys
|
||||
null, // status
|
||||
null, // lmsSebRestriction (deprecated)
|
||||
null, // updating
|
||||
null, // lastUpdate
|
||||
BooleanUtils.toIntegerObject(exam.active));
|
||||
|
||||
this.examRecordMapper.updateByPrimaryKeySelective(newRecord);
|
||||
return this.examRecordMapper.selectByPrimaryKey(examRecord.getId());
|
||||
throw new DuplicateResourceException(EntityType.EXAM, exam.externalId);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -577,7 +577,10 @@ public class SEBClientConfigDAOImpl implements SEBClientConfigDAO {
|
|||
EntityType.SEB_CLIENT_CONFIGURATION,
|
||||
configId,
|
||||
SEBClientConfig.ATTR_FALLBACK_PASSWORD,
|
||||
this.clientCredentialService.encrypt(sebClientConfig.fallbackPassword).toString());
|
||||
this.clientCredentialService
|
||||
.encrypt(sebClientConfig.fallbackPassword)
|
||||
.getOrThrow()
|
||||
.toString());
|
||||
} else {
|
||||
this.additionalAttributesDAO.delete(
|
||||
EntityType.SEB_CLIENT_CONFIGURATION,
|
||||
|
@ -590,7 +593,10 @@ public class SEBClientConfigDAOImpl implements SEBClientConfigDAO {
|
|||
EntityType.SEB_CLIENT_CONFIGURATION,
|
||||
configId,
|
||||
SEBClientConfig.ATTR_QUIT_PASSWORD,
|
||||
this.clientCredentialService.encrypt(sebClientConfig.quitPassword).toString());
|
||||
this.clientCredentialService
|
||||
.encrypt(sebClientConfig.quitPassword)
|
||||
.getOrThrow()
|
||||
.toString());
|
||||
} else {
|
||||
this.additionalAttributesDAO.delete(
|
||||
EntityType.SEB_CLIENT_CONFIGURATION,
|
||||
|
|
|
@ -476,12 +476,8 @@ public class MoodleCourseAccess extends AbstractCourseAccess {
|
|||
|
||||
final Map<String, CourseData> finalCourseDataRef = courseData;
|
||||
courseQuizData.quizzes
|
||||
.forEach(quiz -> {
|
||||
final CourseData course = finalCourseDataRef.get(quiz.course);
|
||||
if (course != null) {
|
||||
course.quizzes.add(quiz);
|
||||
}
|
||||
});
|
||||
.stream()
|
||||
.forEach(quiz -> fillSelectedQuizzes(quizIds, finalCourseDataRef, quiz));
|
||||
|
||||
final String urlPrefix = (lmsSetup.lmsApiUrl.endsWith(Constants.URL_PATH_SEPARATOR))
|
||||
? lmsSetup.lmsApiUrl + MOODLE_QUIZ_START_URL_PATH
|
||||
|
@ -510,6 +506,27 @@ public class MoodleCourseAccess extends AbstractCourseAccess {
|
|||
}
|
||||
}
|
||||
|
||||
private void fillSelectedQuizzes(
|
||||
final Set<String> quizIds,
|
||||
final Map<String, CourseData> finalCourseDataRef,
|
||||
final CourseQuiz quiz) {
|
||||
try {
|
||||
final CourseData course = finalCourseDataRef.get(quiz.course);
|
||||
if (course != null) {
|
||||
final String internalQuizId = getInternalQuizId(
|
||||
quiz.course_module,
|
||||
course.id,
|
||||
course.short_name,
|
||||
course.idnumber);
|
||||
if (quizIds.contains(internalQuizId)) {
|
||||
course.quizzes.add(quiz);
|
||||
}
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to verify selected quiz for course: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private Collection<CourseData> getCoursesForIds(
|
||||
final MoodleAPIRestTemplate restTemplate,
|
||||
final Set<String> ids) {
|
||||
|
|
|
@ -517,7 +517,10 @@ public class ExamConfigXMLParser extends DefaultHandler {
|
|||
attribute.id,
|
||||
listIndex,
|
||||
StringUtils.isNotBlank(value)
|
||||
? this.cryptor.encrypt(value + Constants.IMPORTED_PASSWORD_MARKER).toString()
|
||||
? this.cryptor
|
||||
.encrypt(value + Constants.IMPORTED_PASSWORD_MARKER)
|
||||
.getOrThrow()
|
||||
.toString()
|
||||
: value);
|
||||
}
|
||||
|
||||
|
|
|
@ -133,7 +133,10 @@ public class StringConverter implements AttributeValueConverter {
|
|||
|
||||
// decrypt internally encrypted password and hash it for export
|
||||
// NOTE: see special case description in ExamConfigXMLParser.createConfigurationValue
|
||||
final String plainText = this.clientCredentialService.decrypt(value).toString();
|
||||
final String plainText = this.clientCredentialService
|
||||
.decrypt(value)
|
||||
.getOrThrow()
|
||||
.toString();
|
||||
if (plainText.endsWith(Constants.IMPORTED_PASSWORD_MARKER)) {
|
||||
return plainText.replace(Constants.IMPORTED_PASSWORD_MARKER, StringUtils.EMPTY);
|
||||
} else {
|
||||
|
|
|
@ -27,8 +27,9 @@ public interface ClientIndicator extends IndicatorValue {
|
|||
*
|
||||
* @param indicatorDefinition The indicator definition that defines type and thresholds of the indicator
|
||||
* @param connectionId the connection identifier to that this ClientIndicator is associated to
|
||||
* @param active indicates whether the connection is still an a active state or not
|
||||
* @param cachingEnabled defines whether indicator value caching is enabled or not. */
|
||||
void init(Indicator indicatorDefinition, Long connectionId, boolean cachingEnabled);
|
||||
void init(Indicator indicatorDefinition, Long connectionId, boolean active, boolean cachingEnabled);
|
||||
|
||||
/** Get the exam identifier of the client connection of this ClientIndicator
|
||||
*
|
||||
|
|
|
@ -75,7 +75,7 @@ public class ClientConnectionDataInternal extends ClientConnectionData {
|
|||
@Override
|
||||
@JsonProperty(ATTR_MISSING_PING)
|
||||
public Boolean getMissingPing() {
|
||||
return this.pingIndicator.isMissingPing();
|
||||
return this.pingIndicator != null && this.pingIndicator.isMissingPing();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -81,6 +81,7 @@ public class ClientIndicatorFactory {
|
|||
indicator.init(
|
||||
indicatorDef,
|
||||
clientConnection.id,
|
||||
clientConnection.status.clientActiveStatus,
|
||||
this.enableCaching);
|
||||
|
||||
result.add(indicator);
|
||||
|
@ -105,9 +106,11 @@ public class ClientIndicatorFactory {
|
|||
null,
|
||||
null,
|
||||
Arrays.asList(new Indicator.Threshold(5000d, null, null)));
|
||||
|
||||
pingIndicator.init(
|
||||
indicator,
|
||||
clientConnection.id,
|
||||
clientConnection.status.clientActiveStatus,
|
||||
this.enableCaching);
|
||||
result.add(pingIndicator);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -10,25 +10,18 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl;
|
|||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
|
||||
import org.mybatis.dynamic.sql.SqlBuilder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.cache.annotation.CacheEvict;
|
||||
import org.springframework.cache.annotation.Cacheable;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamStatus;
|
||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
|
||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.EventType;
|
||||
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||
import ch.ethz.seb.sebserver.gbl.util.Utils;
|
||||
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientEventRecordDynamicSqlSupport;
|
||||
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientEventRecordMapper;
|
||||
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientEventRecord;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientConnectionDAO;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.RemoteProctoringRoomDAO;
|
||||
|
@ -48,7 +41,6 @@ public class ExamSessionCacheService {
|
|||
public static final String CACHE_NAME_RUNNING_EXAM = "RUNNING_EXAM";
|
||||
public static final String CACHE_NAME_ACTIVE_CLIENT_CONNECTION = "ACTIVE_CLIENT_CONNECTION";
|
||||
public static final String CACHE_NAME_SEB_CONFIG_EXAM = "SEB_CONFIG_EXAM";
|
||||
public static final String CACHE_NAME_PING_RECORD = "CACHE_NAME_PING_RECORD";
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ExamSessionCacheService.class);
|
||||
|
||||
|
@ -56,7 +48,6 @@ public class ExamSessionCacheService {
|
|||
private final ClientConnectionDAO clientConnectionDAO;
|
||||
private final InternalClientConnectionDataFactory internalClientConnectionDataFactory;
|
||||
private final ExamConfigService sebExamConfigService;
|
||||
private final ClientEventRecordMapper clientEventRecordMapper;
|
||||
private final ExamUpdateHandler examUpdateHandler;
|
||||
|
||||
protected ExamSessionCacheService(
|
||||
|
@ -64,7 +55,6 @@ public class ExamSessionCacheService {
|
|||
final ClientConnectionDAO clientConnectionDAO,
|
||||
final InternalClientConnectionDataFactory internalClientConnectionDataFactory,
|
||||
final ExamConfigService sebExamConfigService,
|
||||
final ClientEventRecordMapper clientEventRecordMapper,
|
||||
final ExamUpdateHandler examUpdateHandler,
|
||||
final RemoteProctoringRoomDAO remoteProctoringRoomDAO) {
|
||||
|
||||
|
@ -72,7 +62,6 @@ public class ExamSessionCacheService {
|
|||
this.clientConnectionDAO = clientConnectionDAO;
|
||||
this.internalClientConnectionDataFactory = internalClientConnectionDataFactory;
|
||||
this.sebExamConfigService = sebExamConfigService;
|
||||
this.clientEventRecordMapper = clientEventRecordMapper;
|
||||
this.examUpdateHandler = examUpdateHandler;
|
||||
}
|
||||
|
||||
|
@ -125,7 +114,7 @@ public class ExamSessionCacheService {
|
|||
}
|
||||
|
||||
public boolean isRunning(final Exam exam) {
|
||||
if (exam == null) {
|
||||
if (exam == null || !exam.active) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -202,56 +191,13 @@ public class ExamSessionCacheService {
|
|||
}
|
||||
}
|
||||
|
||||
@Cacheable(
|
||||
cacheNames = CACHE_NAME_PING_RECORD,
|
||||
key = "#connectionToken",
|
||||
unless = "#result == null")
|
||||
@Transactional
|
||||
public ClientEventRecord getPingRecord(final String connectionToken) {
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Verify ClientConnection for ping record to cache by connectionToken: {}", connectionToken);
|
||||
}
|
||||
|
||||
final ClientConnection clientConnection = getClientConnectionByToken(connectionToken);
|
||||
if (clientConnection == null) {
|
||||
return null;
|
||||
} else {
|
||||
try {
|
||||
return this.clientEventRecordMapper.selectByExample()
|
||||
.where(
|
||||
ClientEventRecordDynamicSqlSupport.clientConnectionId,
|
||||
SqlBuilder.isEqualTo(clientConnection.getId()))
|
||||
.and(
|
||||
ClientEventRecordDynamicSqlSupport.type,
|
||||
SqlBuilder.isEqualTo(EventType.LAST_PING.id))
|
||||
.build()
|
||||
.execute()
|
||||
.stream()
|
||||
.collect(Utils.toSingleton());
|
||||
|
||||
} catch (final Exception e) {
|
||||
log.error("Unexpected error: ", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@CacheEvict(
|
||||
cacheNames = CACHE_NAME_PING_RECORD,
|
||||
key = "#connectionToken")
|
||||
public void evictPingRecord(final String connectionToken) {
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("Eviction of ReusableClientEventRecord from cache for connection token: {}", connectionToken);
|
||||
}
|
||||
}
|
||||
|
||||
private ClientConnection getClientConnectionByToken(final String connectionToken) {
|
||||
final Result<ClientConnection> result = this.clientConnectionDAO
|
||||
.byConnectionToken(connectionToken);
|
||||
|
||||
if (result.hasError()) {
|
||||
log.error("Failed to find/load ClientConnection with connectionToken {}", connectionToken,
|
||||
log.error("Failed to find/load ClientConnection with connectionToken {}",
|
||||
connectionToken,
|
||||
result.getError());
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -15,12 +15,11 @@ import java.util.Collection;
|
|||
import java.util.Collections;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
import org.mybatis.dynamic.sql.SqlBuilder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
|
@ -38,9 +37,6 @@ import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
|
|||
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
|
||||
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||
import ch.ethz.seb.sebserver.webservice.datalayer.batis.ClientConnectionMinMapper;
|
||||
import ch.ethz.seb.sebserver.webservice.datalayer.batis.ClientConnectionMinMapper.ClientConnectionMinRecord;
|
||||
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientConnectionRecordDynamicSqlSupport;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientConnectionDAO;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamConfigurationMapDAO;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO;
|
||||
|
@ -49,7 +45,6 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.IndicatorDAO;
|
|||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionService;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.indicator.IndicatorDistributedRequestCache;
|
||||
|
||||
@Lazy
|
||||
@Service
|
||||
|
@ -59,37 +54,33 @@ public class ExamSessionServiceImpl implements ExamSessionService {
|
|||
private static final Logger log = LoggerFactory.getLogger(ExamSessionServiceImpl.class);
|
||||
|
||||
private final ClientConnectionDAO clientConnectionDAO;
|
||||
private final ClientConnectionMinMapper clientConnectionMinMapper;
|
||||
private final IndicatorDAO indicatorDAO;
|
||||
private final ExamSessionCacheService examSessionCacheService;
|
||||
private final ExamDAO examDAO;
|
||||
private final ExamConfigurationMapDAO examConfigurationMapDAO;
|
||||
private final CacheManager cacheManager;
|
||||
private final SEBRestrictionService sebRestrictionService;
|
||||
private final IndicatorDistributedRequestCache indicatorDistributedRequestCache;
|
||||
private final boolean distributedSetup;
|
||||
|
||||
private long lastConnectionTokenCacheUpdate = 0;
|
||||
|
||||
protected ExamSessionServiceImpl(
|
||||
final ExamSessionCacheService examSessionCacheService,
|
||||
final ClientConnectionMinMapper clientConnectionMinMapper,
|
||||
final ExamDAO examDAO,
|
||||
final ExamConfigurationMapDAO examConfigurationMapDAO,
|
||||
final ClientConnectionDAO clientConnectionDAO,
|
||||
final IndicatorDAO indicatorDAO,
|
||||
final CacheManager cacheManager,
|
||||
final SEBRestrictionService sebRestrictionService,
|
||||
final IndicatorDistributedRequestCache indicatorDistributedRequestCache,
|
||||
@Value("${sebserver.webservice.distributed:false}") final boolean distributedSetup) {
|
||||
|
||||
this.examSessionCacheService = examSessionCacheService;
|
||||
this.clientConnectionMinMapper = clientConnectionMinMapper;
|
||||
this.examDAO = examDAO;
|
||||
this.examConfigurationMapDAO = examConfigurationMapDAO;
|
||||
this.clientConnectionDAO = clientConnectionDAO;
|
||||
this.cacheManager = cacheManager;
|
||||
this.indicatorDAO = indicatorDAO;
|
||||
this.sebRestrictionService = sebRestrictionService;
|
||||
this.indicatorDistributedRequestCache = indicatorDistributedRequestCache;
|
||||
this.distributedSetup = distributedSetup;
|
||||
}
|
||||
|
||||
|
@ -324,21 +315,11 @@ public class ExamSessionServiceImpl implements ExamSessionService {
|
|||
|
||||
final ClientConnectionDataInternal activeClientConnection = this.examSessionCacheService
|
||||
.getClientConnection(connectionToken);
|
||||
|
||||
if (activeClientConnection == null) {
|
||||
throw new NoSuchElementException("Client Connection with token: " + connectionToken);
|
||||
}
|
||||
|
||||
if (this.distributedSetup) {
|
||||
|
||||
final Boolean upToDate = this.clientConnectionDAO
|
||||
.isUpToDate(activeClientConnection.clientConnection)
|
||||
.getOr(false);
|
||||
if (!upToDate) {
|
||||
this.examSessionCacheService.evictClientConnection(connectionToken);
|
||||
return this.examSessionCacheService.getClientConnection(connectionToken);
|
||||
}
|
||||
}
|
||||
|
||||
return activeClientConnection;
|
||||
|
||||
});
|
||||
|
@ -349,35 +330,20 @@ public class ExamSessionServiceImpl implements ExamSessionService {
|
|||
final Long examId,
|
||||
final Predicate<ClientConnectionData> filter) {
|
||||
|
||||
if (this.distributedSetup) {
|
||||
return Result.tryCatch(() -> {
|
||||
|
||||
// if we run in distributed mode, we have to get the connection tokens of the exam
|
||||
// always from the persistent storage and update the client connection cache
|
||||
// before by remove out-dated client connection. This is done within the update_time
|
||||
// of the client connection record that is set on every update in the persistent
|
||||
// storage. So if the update_time of the cached client connection doesen't match the
|
||||
// update_time from persistent, we need to flush this particular client connection from the cache
|
||||
this.indicatorDistributedRequestCache.evictPingTimes(examId);
|
||||
return Result.tryCatch(() -> this.clientConnectionMinMapper.selectByExample()
|
||||
.where(
|
||||
ClientConnectionRecordDynamicSqlSupport.examId,
|
||||
SqlBuilder.isEqualTo(examId))
|
||||
.build()
|
||||
.execute()
|
||||
.stream()
|
||||
.map(this.distributedClientConnectionUpdateFunction(filter))
|
||||
.filter(filter)
|
||||
.collect(Collectors.toList()));
|
||||
updateClientConnections(examId);
|
||||
|
||||
} else {
|
||||
return Result.tryCatch(() -> this.clientConnectionDAO
|
||||
return this.clientConnectionDAO
|
||||
.getConnectionTokens(examId)
|
||||
.getOrThrow()
|
||||
.stream()
|
||||
.map(this.examSessionCacheService::getClientConnection)
|
||||
.map(token -> getConnectionData(token).getOr(null))
|
||||
.filter(Objects::nonNull)
|
||||
.filter(filter)
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
.collect(Collectors.toList());
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -415,32 +381,43 @@ public class ExamSessionServiceImpl implements ExamSessionService {
|
|||
.forEach(token -> {
|
||||
// evict client connection
|
||||
this.examSessionCacheService.evictClientConnection(token);
|
||||
// evict also cached ping record
|
||||
this.examSessionCacheService.evictPingRecord(token);
|
||||
});
|
||||
|
||||
return exam;
|
||||
});
|
||||
}
|
||||
|
||||
private Function<ClientConnectionMinRecord, ClientConnectionDataInternal> distributedClientConnectionUpdateFunction(
|
||||
final Predicate<ClientConnectionData> filter) {
|
||||
// If we are in a distributed setup the active connection token cache get flushed
|
||||
// at least every second. This allows caching over multiple monitoring requests but
|
||||
// ensure an update every second for new incoming connections
|
||||
private void updateClientConnections(final Long examId) {
|
||||
|
||||
return cd -> {
|
||||
ClientConnectionDataInternal clientConnection = this.examSessionCacheService
|
||||
.getClientConnection(cd.connection_token);
|
||||
try {
|
||||
if (this.distributedSetup &&
|
||||
System.currentTimeMillis() - this.lastConnectionTokenCacheUpdate > Constants.SECOND_IN_MILLIS) {
|
||||
|
||||
if (filter.test(clientConnection)) {
|
||||
if (cd.update_time != null &&
|
||||
!cd.update_time.equals(clientConnection.clientConnection.updateTime)) {
|
||||
// go trough all client connection and update the ones that not up to date
|
||||
this.clientConnectionDAO.evictConnectionTokenCache(examId);
|
||||
|
||||
this.examSessionCacheService.evictClientConnection(cd.connection_token);
|
||||
clientConnection = this.examSessionCacheService
|
||||
.getClientConnection(cd.connection_token);
|
||||
}
|
||||
final Set<Long> timestamps = this.clientConnectionDAO
|
||||
.getConnectionTokens(examId)
|
||||
.getOrThrow()
|
||||
.stream()
|
||||
.map(this.examSessionCacheService::getClientConnection)
|
||||
.filter(Objects::nonNull)
|
||||
.map(cc -> cc.getClientConnection().updateTime)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
this.clientConnectionDAO.getClientConnectionsOutOfSyc(examId, timestamps)
|
||||
.getOrElse(() -> Collections.emptySet())
|
||||
.stream()
|
||||
.forEach(this.examSessionCacheService::evictClientConnection);
|
||||
|
||||
this.lastConnectionTokenCacheUpdate = System.currentTimeMillis();
|
||||
}
|
||||
return clientConnection;
|
||||
};
|
||||
} catch (final Exception e) {
|
||||
log.error("Unexpected error while trying to update client connections: ", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,8 +16,6 @@ import java.util.function.Predicate;
|
|||
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.DateTimeZone;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.cache.Cache;
|
||||
|
@ -35,6 +33,7 @@ import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
|
|||
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.EventType;
|
||||
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||
import ch.ethz.seb.sebserver.gbl.util.Utils;
|
||||
import ch.ethz.seb.sebserver.webservice.WebserviceInfo;
|
||||
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientConnectionRecord;
|
||||
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientEventRecord;
|
||||
|
@ -43,7 +42,6 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.SEBClientConfigDAO;
|
|||
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamAdminService;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.EventHandlingStrategy;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.PingHandlingStrategy;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientConnectionService;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientInstructionService;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientNotificationService;
|
||||
|
@ -68,7 +66,6 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
|||
private final CacheManager cacheManager;
|
||||
private final EventHandlingStrategy eventHandlingStrategy;
|
||||
private final ClientConnectionDAO clientConnectionDAO;
|
||||
private final PingHandlingStrategy pingHandlingStrategy;
|
||||
private final SEBClientConfigDAO sebClientConfigDAO;
|
||||
private final SEBClientInstructionService sebInstructionService;
|
||||
private final SEBClientNotificationService sebClientNotificationService;
|
||||
|
@ -78,7 +75,7 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
|||
protected SEBClientConnectionServiceImpl(
|
||||
final ExamSessionService examSessionService,
|
||||
final EventHandlingStrategyFactory eventHandlingStrategyFactory,
|
||||
final PingHandlingStrategyFactory pingHandlingStrategyFactory,
|
||||
|
||||
final SEBClientConfigDAO sebClientConfigDAO,
|
||||
final SEBClientInstructionService sebInstructionService,
|
||||
final SEBClientNotificationService sebClientNotificationService,
|
||||
|
@ -88,7 +85,6 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
|||
this.examSessionCacheService = examSessionService.getExamSessionCacheService();
|
||||
this.cacheManager = examSessionService.getCacheManager();
|
||||
this.clientConnectionDAO = examSessionService.getClientConnectionDAO();
|
||||
this.pingHandlingStrategy = pingHandlingStrategyFactory.get();
|
||||
this.eventHandlingStrategy = eventHandlingStrategyFactory.get();
|
||||
this.sebClientConfigDAO = sebClientConfigDAO;
|
||||
this.sebInstructionService = sebInstructionService;
|
||||
|
@ -368,11 +364,6 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
|||
updatedClientConnection);
|
||||
}
|
||||
|
||||
// notify ping handler about established connection
|
||||
this.pingHandlingStrategy.initForConnection(
|
||||
updatedClientConnection.id,
|
||||
connectionToken);
|
||||
|
||||
return updatedClientConnection;
|
||||
});
|
||||
}
|
||||
|
@ -520,7 +511,8 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
|||
try {
|
||||
|
||||
final Cache cache = this.cacheManager.getCache(ExamSessionCacheService.CACHE_NAME_ACTIVE_CLIENT_CONNECTION);
|
||||
final long now = DateTime.now(DateTimeZone.UTC).getMillis();
|
||||
final long now = Utils.getMillisecondsNow();
|
||||
final Consumer<ClientConnectionDataInternal> missingPingUpdate = missingPingUpdate(now);
|
||||
this.examSessionService
|
||||
.getExamDAO()
|
||||
.allRunningExamIds()
|
||||
|
@ -538,8 +530,8 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
|||
.map(token -> cache.get(token, ClientConnectionDataInternal.class))
|
||||
.filter(Objects::nonNull)
|
||||
.filter(connection -> connection.pingIndicator != null &&
|
||||
connection.clientConnection.status.establishedStatus)
|
||||
.forEach(missingPingUpdate(now));
|
||||
connection.clientConnection.status.clientActiveStatus)
|
||||
.forEach(connection -> missingPingUpdate.accept(connection));
|
||||
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to update ping events: ", e);
|
||||
|
@ -557,7 +549,13 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
|||
final long timestamp,
|
||||
final int pingNumber) {
|
||||
|
||||
this.pingHandlingStrategy.notifyPing(connectionToken, timestamp, pingNumber);
|
||||
final ClientConnectionDataInternal activeClientConnection =
|
||||
this.examSessionCacheService.getClientConnection(connectionToken);
|
||||
|
||||
if (activeClientConnection != null) {
|
||||
activeClientConnection.notifyPing(timestamp, pingNumber);
|
||||
}
|
||||
|
||||
return this.sebInstructionService.getInstructionJSON(connectionToken);
|
||||
}
|
||||
|
||||
|
@ -732,8 +730,6 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
|||
private ClientConnectionDataInternal reloadConnectionCache(final String connectionToken) {
|
||||
// evict cached ClientConnection
|
||||
this.examSessionCacheService.evictClientConnection(connectionToken);
|
||||
// evict also cached ping record
|
||||
this.examSessionCacheService.evictPingRecord(connectionToken);
|
||||
// and load updated ClientConnection into cache
|
||||
return this.examSessionCacheService.getClientConnection(connectionToken);
|
||||
}
|
||||
|
|
|
@ -48,6 +48,7 @@ public class SEBClientInstructionServiceImpl implements SEBClientInstructionServ
|
|||
|
||||
private static final Logger log = LoggerFactory.getLogger(SEBClientInstructionServiceImpl.class);
|
||||
|
||||
private static final long PERSISTENT_UPDATE_INTERVAL = 2 * Constants.SECOND_IN_MILLIS;
|
||||
private static final int INSTRUCTION_QUEUE_MAX_SIZE = 10;
|
||||
private static final String JSON_INST = "instruction";
|
||||
private static final String JSON_ATTR = "attributes";
|
||||
|
@ -281,9 +282,9 @@ public class SEBClientInstructionServiceImpl implements SEBClientInstructionServ
|
|||
|
||||
// Since the queue is empty check periodically if there are active instructions on the persistent storage
|
||||
final long currentTimeMillis = System.currentTimeMillis();
|
||||
if (currentTimeMillis - this.lastRefresh > Constants.SECOND_IN_MILLIS) {
|
||||
if (currentTimeMillis - this.lastRefresh > PERSISTENT_UPDATE_INTERVAL) {
|
||||
this.lastRefresh = currentTimeMillis;
|
||||
loadInstructions(connectionToken)
|
||||
loadInstructions()
|
||||
.onError(error -> log.error(
|
||||
"Failed load instructions from persistent storage and to refresh cache: ",
|
||||
error));
|
||||
|
@ -300,8 +301,10 @@ public class SEBClientInstructionServiceImpl implements SEBClientInstructionServ
|
|||
// check if there are still queues in the cache, whether they are empty or not,
|
||||
// for closed or disposed client connections and remove them from cache
|
||||
synchronized (this.instructions) {
|
||||
|
||||
final Result<Collection<String>> result = this.clientConnectionDAO
|
||||
.getInactiveConnctionTokens(this.instructions.keySet());
|
||||
|
||||
if (result.hasValue()) {
|
||||
result.get().stream().forEach(token -> this.instructions.remove(token));
|
||||
}
|
||||
|
@ -325,12 +328,6 @@ public class SEBClientInstructionServiceImpl implements SEBClientInstructionServ
|
|||
}
|
||||
}
|
||||
|
||||
private Result<Void> loadInstructions(final String connectionToken) {
|
||||
return Result.tryCatch(() -> this.clientInstructionDAO.getAllActive(connectionToken)
|
||||
.getOrThrow()
|
||||
.forEach(this::putToCacheIfAbsent));
|
||||
}
|
||||
|
||||
private Result<Void> loadInstructions() {
|
||||
return Result.tryCatch(() -> this.clientInstructionDAO.getAllActive()
|
||||
.getOrThrow()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -8,18 +8,22 @@
|
|||
|
||||
package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.indicator;
|
||||
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.DateTimeZone;
|
||||
import org.joda.time.DateTimeUtils;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.Constants;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.Indicator;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ClientIndicator;
|
||||
|
||||
public abstract class AbstractClientIndicator implements ClientIndicator {
|
||||
|
||||
private static final long PERSISTENT_UPDATE_INTERVAL = Constants.SECOND_IN_MILLIS;
|
||||
|
||||
protected Long indicatorId;
|
||||
protected Long examId;
|
||||
protected Long connectionId;
|
||||
protected boolean cachingEnabled;
|
||||
protected boolean active = true;
|
||||
protected long lastPersistentUpdate = 0;
|
||||
|
||||
protected boolean valueInitializes = false;
|
||||
protected double currentValue = Double.NaN;
|
||||
|
@ -28,11 +32,13 @@ public abstract class AbstractClientIndicator implements ClientIndicator {
|
|||
public void init(
|
||||
final Indicator indicatorDefinition,
|
||||
final Long connectionId,
|
||||
final boolean active,
|
||||
final boolean cachingEnabled) {
|
||||
|
||||
this.indicatorId = (indicatorDefinition != null) ? indicatorDefinition.id : -1;
|
||||
this.examId = (indicatorDefinition != null) ? indicatorDefinition.examId : -1;
|
||||
this.connectionId = connectionId;
|
||||
this.active = active;
|
||||
this.cachingEnabled = cachingEnabled;
|
||||
}
|
||||
|
||||
|
@ -58,11 +64,20 @@ public abstract class AbstractClientIndicator implements ClientIndicator {
|
|||
|
||||
@Override
|
||||
public double getValue() {
|
||||
if (!this.valueInitializes || !this.cachingEnabled) {
|
||||
this.currentValue = computeValueAt(DateTime.now(DateTimeZone.UTC).getMillis());
|
||||
final long now = DateTimeUtils.currentTimeMillis();
|
||||
if (!this.valueInitializes) {
|
||||
this.currentValue = computeValueAt(now);
|
||||
this.lastPersistentUpdate = now;
|
||||
this.valueInitializes = true;
|
||||
}
|
||||
|
||||
if (!this.cachingEnabled && this.active) {
|
||||
if (now - this.lastPersistentUpdate > PERSISTENT_UPDATE_INTERVAL) {
|
||||
this.currentValue = computeValueAt(now);
|
||||
this.lastPersistentUpdate = now;
|
||||
}
|
||||
}
|
||||
|
||||
return this.currentValue;
|
||||
}
|
||||
|
||||
|
|
|
@ -37,8 +37,14 @@ public abstract class AbstractLogIndicator extends AbstractClientIndicator {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void init(final Indicator indicatorDefinition, final Long connectionId, final boolean cachingEnabled) {
|
||||
super.init(indicatorDefinition, connectionId, cachingEnabled);
|
||||
public void init(
|
||||
final Indicator indicatorDefinition,
|
||||
final Long connectionId,
|
||||
final boolean active,
|
||||
final boolean cachingEnabled) {
|
||||
|
||||
super.init(indicatorDefinition, connectionId, active, cachingEnabled);
|
||||
|
||||
if (indicatorDefinition == null || StringUtils.isBlank(indicatorDefinition.tags)) {
|
||||
this.tags = null;
|
||||
} else {
|
||||
|
|
|
@ -56,14 +56,11 @@ public abstract class AbstractLogLevelCountIndicator extends AbstractLogIndicato
|
|||
|
||||
@Override
|
||||
public double computeValueAt(final long timestamp) {
|
||||
|
||||
try {
|
||||
|
||||
// TODO to boost performance here within a distributed setup, invent a new cache for all log count values
|
||||
// of the running exam. So all indicators get the values from cache and only one single SQL call
|
||||
// is needed for one update.
|
||||
// This cache then is only valid for one (GUI) update cycle and the cache must to be flushed before
|
||||
|
||||
final Long errors = this.clientEventRecordMapper.countByExample()
|
||||
final Long errors = this.clientEventRecordMapper
|
||||
.countByExample()
|
||||
.where(ClientEventRecordDynamicSqlSupport.clientConnectionId, isEqualTo(this.connectionId))
|
||||
.and(ClientEventRecordDynamicSqlSupport.type, isIn(this.eventTypeIds))
|
||||
.and(ClientEventRecordDynamicSqlSupport.serverTime, isLessThan(timestamp))
|
||||
|
|
|
@ -64,11 +64,6 @@ public abstract class AbstractLogNumberIndicator extends AbstractLogIndicator {
|
|||
public double computeValueAt(final long timestamp) {
|
||||
try {
|
||||
|
||||
// TODO to boost performance here within a distributed setup, invent a new cache for all log count values
|
||||
// of the running exam. So all indicators get the values from cache and only one single SQL call
|
||||
// is needed for one update.
|
||||
// This cache then is only valid for one (GUI) update cycle and the cache must to be flushed before
|
||||
|
||||
final List<ClientEventRecord> execute = this.clientEventRecordMapper.selectByExample()
|
||||
.where(ClientEventRecordDynamicSqlSupport.clientConnectionId, isEqualTo(this.connectionId))
|
||||
.and(ClientEventRecordDynamicSqlSupport.type, isIn(this.eventTypeIds))
|
||||
|
|
|
@ -13,54 +13,61 @@ import java.util.EnumSet;
|
|||
import java.util.Set;
|
||||
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.DateTimeUtils;
|
||||
import org.joda.time.DateTimeZone;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.Constants;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.Indicator;
|
||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.EventType;
|
||||
import ch.ethz.seb.sebserver.webservice.datalayer.batis.ClientEventExtensionMapper;
|
||||
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientEventRecord;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientEventDAO;
|
||||
|
||||
public abstract class AbstractPingIndicator extends AbstractClientIndicator {
|
||||
|
||||
private static final long INTERVAL_FOR_PERSISTENT_UPDATE = Constants.SECOND_IN_MILLIS;
|
||||
|
||||
private final Set<EventType> EMPTY_SET = Collections.unmodifiableSet(EnumSet.noneOf(EventType.class));
|
||||
|
||||
protected final ClientEventExtensionMapper clientEventExtensionMapper;
|
||||
protected final IndicatorDistributedRequestCache indicatorDistributedRequestCache;
|
||||
protected final ClientEventDAO clientEventDAO;
|
||||
|
||||
protected long pingLatency;
|
||||
protected int pingCount = 0;
|
||||
protected int pingNumber = 0;
|
||||
|
||||
protected AbstractPingIndicator(
|
||||
final ClientEventExtensionMapper clientEventExtensionMapper,
|
||||
final IndicatorDistributedRequestCache indicatorDistributedRequestCache) {
|
||||
private long lastUpdate = 0;
|
||||
protected ClientEventRecord pingRecord = null;
|
||||
|
||||
protected AbstractPingIndicator(final ClientEventDAO clientEventDAO) {
|
||||
super();
|
||||
this.clientEventExtensionMapper = clientEventExtensionMapper;
|
||||
this.indicatorDistributedRequestCache = indicatorDistributedRequestCache;
|
||||
this.clientEventDAO = clientEventDAO;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(
|
||||
final Indicator indicatorDefinition,
|
||||
final Long connectionId,
|
||||
final boolean active,
|
||||
final boolean cachingEnabled) {
|
||||
|
||||
super.init(indicatorDefinition, connectionId, active, cachingEnabled);
|
||||
|
||||
if (!this.cachingEnabled) {
|
||||
this.pingRecord = this.clientEventDAO
|
||||
.initPingEvent(this.connectionId)
|
||||
.getOr(null);
|
||||
}
|
||||
}
|
||||
|
||||
public final void notifyPing(final long timestamp, final int pingNumber) {
|
||||
final long now = DateTime.now(DateTimeZone.UTC).getMillis();
|
||||
this.pingLatency = now - timestamp;
|
||||
super.currentValue = now;
|
||||
this.pingCount++;
|
||||
this.pingNumber = pingNumber;
|
||||
}
|
||||
super.lastPersistentUpdate = now;
|
||||
|
||||
@Override
|
||||
public final double computeValueAt(final long timestamp) {
|
||||
if (this.cachingEnabled) {
|
||||
return timestamp;
|
||||
} else {
|
||||
try {
|
||||
return this.indicatorDistributedRequestCache
|
||||
.getPingTimes(this.examId)
|
||||
.getOrDefault(this.connectionId, 0L);
|
||||
if (!this.cachingEnabled && this.pingRecord != null) {
|
||||
|
||||
} catch (final Exception e) {
|
||||
return Double.NaN;
|
||||
// Update last ping time on persistent storage
|
||||
final long millisecondsNow = DateTimeUtils.currentTimeMillis();
|
||||
if (millisecondsNow - this.lastUpdate > INTERVAL_FOR_PERSISTENT_UPDATE) {
|
||||
this.pingRecord.setClientTime(timestamp);
|
||||
this.pingRecord.setServerTime(millisecondsNow);
|
||||
this.clientEventDAO.updatePingEvent(this.pingRecord);
|
||||
this.lastUpdate = millisecondsNow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -70,16 +77,6 @@ public abstract class AbstractPingIndicator extends AbstractClientIndicator {
|
|||
return this.EMPTY_SET;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public int getPingCount() {
|
||||
return this.pingCount;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public int getPingNumber() {
|
||||
return this.pingNumber;
|
||||
}
|
||||
|
||||
public abstract ClientEventRecord updateLogEvent(final long now);
|
||||
|
||||
}
|
||||
|
|
|
@ -30,8 +30,13 @@ public class BatteryStatusIndicator extends AbstractLogNumberIndicator {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void init(final Indicator indicatorDefinition, final Long connectionId, final boolean cachingEnabled) {
|
||||
super.init(indicatorDefinition, connectionId, cachingEnabled);
|
||||
public void init(
|
||||
final Indicator indicatorDefinition,
|
||||
final Long connectionId,
|
||||
final boolean active,
|
||||
final boolean cachingEnabled) {
|
||||
|
||||
super.init(indicatorDefinition, connectionId, active, cachingEnabled);
|
||||
super.tags = new String[] { API.LOG_EVENT_TAG_BATTERY_STATUS };
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -11,8 +11,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.indicator;
|
|||
import java.math.BigDecimal;
|
||||
import java.util.Comparator;
|
||||
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.DateTimeZone;
|
||||
import org.joda.time.DateTimeUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
|
||||
|
@ -27,9 +26,9 @@ import ch.ethz.seb.sebserver.gbl.model.exam.Indicator;
|
|||
import ch.ethz.seb.sebserver.gbl.model.exam.Indicator.IndicatorType;
|
||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
|
||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.EventType;
|
||||
import ch.ethz.seb.sebserver.gbl.util.Utils;
|
||||
import ch.ethz.seb.sebserver.webservice.datalayer.batis.ClientEventExtensionMapper;
|
||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientEventRecord;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientEventDAO;
|
||||
|
||||
@Lazy
|
||||
@Component(IndicatorType.Names.LAST_PING)
|
||||
|
@ -38,22 +37,30 @@ public final class PingIntervalClientIndicator extends AbstractPingIndicator {
|
|||
|
||||
private static final Logger log = LoggerFactory.getLogger(PingIntervalClientIndicator.class);
|
||||
|
||||
long pingErrorThreshold;
|
||||
boolean missingPing = false;
|
||||
boolean hidden = false;
|
||||
// This is the default ping error threshold that is set if the threshold cannot be get
|
||||
// from the ping threshold settings. If the last ping is older then this interval back in time
|
||||
// then the ping is considered and marked as missing
|
||||
private static final long DEFAULT_PING_ERROR_THRESHOLD = Constants.SECOND_IN_MILLIS * 5;
|
||||
|
||||
public PingIntervalClientIndicator(
|
||||
final ClientEventExtensionMapper clientEventExtensionMapper,
|
||||
final IndicatorDistributedRequestCache indicatorDistributedRequestCache) {
|
||||
private long pingErrorThreshold;
|
||||
private boolean missingPing = false;
|
||||
private boolean hidden = false;
|
||||
|
||||
super(clientEventExtensionMapper, indicatorDistributedRequestCache);
|
||||
public PingIntervalClientIndicator(final ClientEventDAO clientEventDAO) {
|
||||
super(clientEventDAO);
|
||||
this.cachingEnabled = true;
|
||||
this.currentValue = computeValueAt(Utils.getMillisecondsNow());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(final Indicator indicatorDefinition, final Long connectionId, final boolean cachingEnabled) {
|
||||
super.init(indicatorDefinition, connectionId, cachingEnabled);
|
||||
public void init(
|
||||
final Indicator indicatorDefinition,
|
||||
final Long connectionId,
|
||||
final boolean active,
|
||||
final boolean cachingEnabled) {
|
||||
|
||||
super.init(indicatorDefinition, connectionId, active, cachingEnabled);
|
||||
|
||||
this.currentValue = computeValueAt(DateTimeUtils.currentTimeMillis());
|
||||
|
||||
try {
|
||||
indicatorDefinition
|
||||
|
@ -64,12 +71,13 @@ public final class PingIntervalClientIndicator extends AbstractPingIndicator {
|
|||
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to initialize pingErrorThreshold: {}", e.getMessage());
|
||||
this.pingErrorThreshold = Constants.SECOND_IN_MILLIS * 5;
|
||||
this.pingErrorThreshold = DEFAULT_PING_ERROR_THRESHOLD;
|
||||
}
|
||||
|
||||
if (!cachingEnabled) {
|
||||
try {
|
||||
this.missingPing = this.pingErrorThreshold < getValue();
|
||||
final double value = getValue();
|
||||
this.missingPing = this.pingErrorThreshold < value;
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to initialize missingPing: {}", e.getMessage());
|
||||
this.missingPing = false;
|
||||
|
@ -100,7 +108,7 @@ public final class PingIntervalClientIndicator extends AbstractPingIndicator {
|
|||
|
||||
@Override
|
||||
public double getValue() {
|
||||
final long now = DateTime.now(DateTimeZone.UTC).getMillis();
|
||||
final long now = DateTimeUtils.currentTimeMillis();
|
||||
return now - super.getValue();
|
||||
}
|
||||
|
||||
|
@ -114,6 +122,33 @@ public final class PingIntervalClientIndicator extends AbstractPingIndicator {
|
|||
|
||||
}
|
||||
|
||||
@Override
|
||||
public final double computeValueAt(final long timestamp) {
|
||||
|
||||
if (!this.cachingEnabled && super.pingRecord != null) {
|
||||
|
||||
// if this indicator is not missing ping
|
||||
if (!this.isMissingPing()) {
|
||||
|
||||
final Result<Long> lastPing = this.clientEventDAO
|
||||
.getLastPing(super.pingRecord.getId());
|
||||
|
||||
if (!lastPing.hasError()) {
|
||||
if (Double.isNaN(this.currentValue)) {
|
||||
return lastPing.get().doubleValue();
|
||||
}
|
||||
return Math.max(this.currentValue, lastPing.get().doubleValue());
|
||||
} else {
|
||||
log.error("Failed to get last ping from persistent: {}", lastPing.getError().getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return this.currentValue;
|
||||
}
|
||||
|
||||
return !this.valueInitializes ? timestamp : this.currentValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientEventRecord updateLogEvent(final long now) {
|
||||
final long value = now - (long) super.currentValue;
|
||||
|
|
|
@ -30,8 +30,13 @@ public class WLANStatusIndicator extends AbstractLogNumberIndicator {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void init(final Indicator indicatorDefinition, final Long connectionId, final boolean cachingEnabled) {
|
||||
super.init(indicatorDefinition, connectionId, cachingEnabled);
|
||||
public void init(
|
||||
final Indicator indicatorDefinition,
|
||||
final Long connectionId,
|
||||
final boolean active,
|
||||
final boolean cachingEnabled) {
|
||||
|
||||
super.init(indicatorDefinition, connectionId, active, cachingEnabled);
|
||||
super.tags = new String[] { API.LOG_EVENT_TAG_WLAN_STATUS };
|
||||
}
|
||||
|
||||
|
|
|
@ -280,25 +280,11 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
|
|||
proctoringRoom.id)
|
||||
.getOrThrow();
|
||||
|
||||
final Result<RemoteProctoringRoom> townhallRoomResult = this.remoteProctoringRoomDAO
|
||||
.getTownhallRoom(cc.getExamId());
|
||||
applyProcotringInstruction(
|
||||
cc.getExamId(),
|
||||
cc.getConnectionToken())
|
||||
.getOrThrow();
|
||||
|
||||
if (townhallRoomResult.hasValue()) {
|
||||
final RemoteProctoringRoom townhallRoom = townhallRoomResult.get();
|
||||
applyProcotringInstruction(
|
||||
cc.getExamId(),
|
||||
cc.getConnectionToken(),
|
||||
townhallRoom.name,
|
||||
townhallRoom.subject)
|
||||
.getOrThrow();
|
||||
} else {
|
||||
applyProcotringInstruction(
|
||||
cc.getExamId(),
|
||||
cc.getConnectionToken(),
|
||||
proctoringRoom.name,
|
||||
proctoringRoom.subject)
|
||||
.getOrThrow();
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to assign connection to collecting room: {}", cc, e);
|
||||
}
|
||||
|
@ -601,9 +587,7 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
|
|||
|
||||
private Result<Void> applyProcotringInstruction(
|
||||
final Long examId,
|
||||
final String connectionToken,
|
||||
final String roomName,
|
||||
final String subject) {
|
||||
final String connectionToken) {
|
||||
|
||||
return Result.tryCatch(() -> {
|
||||
final ProctoringServiceSettings settings = this.examAdminService
|
||||
|
@ -614,10 +598,32 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
|
|||
.getExamProctoringService(settings.serverType)
|
||||
.getOrThrow();
|
||||
|
||||
sendJoinCollectingRoomInstructions(
|
||||
settings,
|
||||
Arrays.asList(connectionToken),
|
||||
examProctoringService);
|
||||
final Result<RemoteProctoringRoom> townhallRoomResult = this.remoteProctoringRoomDAO
|
||||
.getTownhallRoom(examId);
|
||||
|
||||
if (townhallRoomResult.hasValue()) {
|
||||
|
||||
final RemoteProctoringRoom townhallRoom = townhallRoomResult.get();
|
||||
final ProctoringRoomConnection roomConnection = examProctoringService.getClientRoomConnection(
|
||||
settings,
|
||||
connectionToken,
|
||||
townhallRoom.name,
|
||||
townhallRoom.subject)
|
||||
.getOrThrow();
|
||||
|
||||
sendJoinInstruction(
|
||||
examId,
|
||||
connectionToken,
|
||||
roomConnection,
|
||||
examProctoringService);
|
||||
} else {
|
||||
|
||||
sendJoinCollectingRoomInstructions(
|
||||
settings,
|
||||
Arrays.asList(connectionToken),
|
||||
examProctoringService);
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -270,7 +270,9 @@ public class ExamAPI_V1_Controller {
|
|||
final String instructionConfirm = request.getParameter(API.EXAM_API_PING_INSTRUCTION_CONFIRM);
|
||||
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("****************** SEB client connection: {}", connectionToken);
|
||||
log.trace("****************** SEB client connection: {} ip: ",
|
||||
connectionToken,
|
||||
getClientAddress(request));
|
||||
}
|
||||
|
||||
if (instructionConfirm != null) {
|
||||
|
@ -352,32 +354,6 @@ public class ExamAPI_V1_Controller {
|
|||
|
||||
final ServletOutputStream outputStream = response.getOutputStream();
|
||||
|
||||
// try {
|
||||
//
|
||||
// final ClientConnectionData connection = this.examSessionService
|
||||
// .getConnectionData(connectionToken)
|
||||
// .getOrThrow();
|
||||
//
|
||||
// // exam integrity check
|
||||
// if (connection.clientConnection.examId == null ||
|
||||
// !this.examSessionService.isExamRunning(connection.clientConnection.examId)) {
|
||||
//
|
||||
// log.error("Missing exam identifier or requested exam is not running for connection: {}",
|
||||
// connection);
|
||||
// throw new IllegalStateException("Missing exam identifier or requested exam is not running");
|
||||
// }
|
||||
// } catch (final Exception e) {
|
||||
//
|
||||
// log.error("Unexpected error: ", e);
|
||||
//
|
||||
// final APIMessage errorMessage = APIMessage.ErrorMessage.GENERIC.of(e.getMessage());
|
||||
// outputStream.write(Utils.toByteArray(this.jsonMapper.writeValueAsString(errorMessage)));
|
||||
// response.setStatus(HttpStatus.BAD_REQUEST.value());
|
||||
// outputStream.flush();
|
||||
// outputStream.close();
|
||||
// return;
|
||||
// }
|
||||
|
||||
try {
|
||||
|
||||
this.examSessionService
|
||||
|
|
|
@ -228,10 +228,10 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
|
|||
defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId) {
|
||||
|
||||
checkReadPrivilege(institutionId);
|
||||
return this.examDAO.allByQuizId(modelId)
|
||||
return this.examDAO.allInstitutionIdsByQuizId(modelId)
|
||||
.map(ids -> ids
|
||||
.stream()
|
||||
.map(id -> new EntityKey(id, EntityType.EXAM))
|
||||
.map(id -> new EntityKey(id, EntityType.INSTITUTION))
|
||||
.collect(Collectors.toList()))
|
||||
.getOrThrow();
|
||||
}
|
||||
|
@ -256,10 +256,6 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
|
|||
.checkExamConsistency(modelId)
|
||||
.getOrThrow();
|
||||
|
||||
if (includeRestriction) {
|
||||
// TODO include seb restriction check and status
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ logging.level.ch=INFO
|
|||
logging.level.org.springframework.cache=INFO
|
||||
logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl=DEBUG
|
||||
logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.session=DEBUG
|
||||
logging.level.ch.ethz.seb.sebserver.webservice.weblayer.api.ExamAPI_V1_Controller=TRACE
|
||||
#logging.level.ch.ethz.seb.sebserver.webservice.weblayer.api.ExamAPI_V1_Controller=TRACE
|
||||
|
||||
sebserver.http.client.connect-timeout=150000
|
||||
sebserver.http.client.connection-request-timeout=100000
|
||||
|
|
|
@ -22,6 +22,7 @@ spring.datasource.url=jdbc:mariadb://${datastore.mariadb.server.address}:${datas
|
|||
spring.flyway.enabled=true
|
||||
spring.flyway.locations=classpath:config/sql/base
|
||||
spring.flyway.cleanDisabled=true
|
||||
spring.flyway.ignoreIgnoredMigrations=true
|
||||
spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
|
||||
spring.datasource.hikari.initializationFailTimeout=3000
|
||||
spring.datasource.hikari.connectionTimeout=30000
|
||||
|
|
|
@ -82,17 +82,6 @@
|
|||
</resources>
|
||||
</cache>
|
||||
|
||||
<cache alias="LAST_PING_TIME_CACHE">
|
||||
<key-type>java.lang.Long</key-type>
|
||||
<value-type>org.ehcache.impl.internal.concurrent.ConcurrentHashMap</value-type>
|
||||
<expiry>
|
||||
<ttl unit="hours">24</ttl>
|
||||
</expiry>
|
||||
<resources>
|
||||
<heap unit="entries">10</heap>
|
||||
</resources>
|
||||
</cache>
|
||||
|
||||
<cache alias="QUIZ_DATA_CACHE">
|
||||
<key-type>java.lang.String</key-type>
|
||||
<value-type>ch.ethz.seb.sebserver.gbl.model.exam.QuizData</value-type>
|
||||
|
|
|
@ -380,6 +380,7 @@ sebserver.quizdiscovery.action.import=Import as Exam
|
|||
sebserver.quizdiscovery.quiz.import.out.dated=The Selected LMS exam is already finished and can't be imported
|
||||
sebserver.quizdiscovery.action.details=Show LMS Exam Details
|
||||
sebserver.quizdiscovery.quiz.import.existing.confirm=This course was already imported and importing it twice may lead to<br/> unexpected behavior within automated SEB restriction on LMS.<br/><br/> Do you want to import this course as exam anyway?
|
||||
sebserver.quizdiscovery.quiz.import.existing=This course was already imported as an exam.<br/>Please find it in the Exam section.
|
||||
|
||||
sebserver.quizdiscovery.quiz.details.title=LMS Exam Details
|
||||
sebserver.quizdiscovery.quiz.details.institution=Institution
|
||||
|
@ -595,7 +596,7 @@ sebserver.exam.indicator.type.WLAN_STATUS=WiFi Status
|
|||
sebserver.exam.indicator.type.description.LAST_PING=This indicator shows the time in milliseconds since<br/> the last ping has been received from a SEB Client.<br/>This indicator can be used to track a SEB Client connection and indicate connection loss.<br/><br/>The value is in milliseconds.
|
||||
sebserver.exam.indicator.type.description.ERROR_COUNT=This indicator shows the number of error log messages that<br/> has been received from a SEB Client.<br/>This indicator can be used to track errors of connected SEB Clients<br/><br/>The value is a natural number.
|
||||
sebserver.exam.indicator.type.description.WARN_COUNT=This indicator shows the number of warn log messages that<br/> has been received from a SEB Client.<br/>This indicator can be used to track warnings of connected SEB Clients<br/><br/>The value is a natural number.
|
||||
sebserver.exam.indicator.type.description.INFO_COUNT=This indicator shows the number of warn log messages that<br/> has been received from a SEB Client.<br/>This indicator can be used to track warnings of connected SEB Clients<br/><br/>The value is a natural number.
|
||||
sebserver.exam.indicator.type.description.INFO_COUNT=This indicator shows the number of info log messages that<br/> has been received from a SEB Client.<br/>This indicator can be used to track warnings of connected SEB Clients<br/><br/>The value is a natural number.
|
||||
sebserver.exam.indicator.type.description.BATTERY_STATUS=This indicator shows the percentage of the battery load level of a SEB Client.
|
||||
sebserver.exam.indicator.type.description.WLAN_STATUS=This indicator shows the percentage of the WiFi connection status of a SEB Client.
|
||||
|
||||
|
@ -1429,7 +1430,7 @@ sebserver.examconfig.props.label.proctoringDetectTalkingDisplay=Feedback for Can
|
|||
sebserver.examconfig.props.label.proctoringDetectTalkingDisplay.tooltips=
|
||||
|
||||
sebserver.examconfig.props.label.remoteProctoringViewShow=Proctoring View Display Policy
|
||||
sebserver.examconfig.props.label.remoteProctoringViewShow.tooltip=
|
||||
sebserver.examconfig.props.label.remoteProctoringViewShow.tooltip=Controls the general policy for the initial display of the proctoring Window in SEB.<br/>Theses setting have no effect on the proctor's broadcast or chat function.
|
||||
sebserver.examconfig.props.label.remoteProctoringViewShow.0=Never
|
||||
sebserver.examconfig.props.label.remoteProctoringViewShow.0.tooltip=
|
||||
sebserver.examconfig.props.label.remoteProctoringViewShow.1=Allow to Show
|
||||
|
@ -1448,23 +1449,24 @@ sebserver.examconfig.props.group.zoom_features.tooltip=
|
|||
sebserver.examconfig.props.group.zoom_controls=Zoom User Controls
|
||||
sebserver.examconfig.props.group.zoom_controls.tooltip=When the proctoring view is disabled (see its display policy on left), users can mute and unmute audio and video manually.<br/>Audio and video streams can be disabled globally with the settings below.
|
||||
sebserver.examconfig.props.label.zoomAudioMuted=Audio Initially Muted
|
||||
sebserver.examconfig.props.label.zoomAudioMuted.tooltip=
|
||||
sebserver.examconfig.props.label.zoomAudioMuted.tooltip=If this option is activated, client proctoring windows will start muted.<br/>The SEB users then can start/stop their audio individually using the respective button in the Zoom windows in SEB.<br/>If the Proctoring View Display Policy is set to "Never", this option will have no effect.
|
||||
sebserver.examconfig.props.label.zoomAudioOnly=Audio Only
|
||||
sebserver.examconfig.props.label.zoomAudioOnly.tooltip=
|
||||
sebserver.examconfig.props.label.zoomAudioOnly.tooltip=If this option is activated, cameras on SEB clients are not active until the proctoring window is opened by the proctor<br/>(and they are deactivated if it is closed again!)
|
||||
sebserver.examconfig.props.label.zoomEnable=Enable Zoom
|
||||
sebserver.examconfig.props.label.zoomEnable.tooltip=
|
||||
sebserver.examconfig.props.label.zoomFeatureFlagChat=Enable Chat
|
||||
sebserver.examconfig.props.label.zoomFeatureFlagChat.tooltip=
|
||||
sebserver.examconfig.props.label.zoomFeatureFlagChat.tooltip=Allows SEB users to use Zoom's chat function in SEB, if the proctor enables chat in the proctoring window on SEB Server
|
||||
sebserver.examconfig.props.label.zoomFeatureFlagCloseCaptions=Enable Close Captions
|
||||
sebserver.examconfig.props.label.zoomFeatureFlagCloseCaptions.tooltip=
|
||||
sebserver.examconfig.props.label.zoomFeatureFlagDisplayingMeetingName=Display Meeting Name
|
||||
sebserver.examconfig.props.label.zoomFeatureFlagDisplayMeetingName.tooltip
|
||||
sebserver.examconfig.props.label.zoomFeatureFlagCloseCaptions.tooltip=Not yet implemented
|
||||
sebserver.examconfig.props.label.zoomFeatureFlagDisplayMeetingName=Display Meeting Name
|
||||
sebserver.examconfig.props.label.zoomFeatureFlagDisplayMeetingName.tooltip=Not yet implemented
|
||||
|
||||
sebserver.examconfig.props.label.zoomFeatureFlagRaiseHand=Enable Raise Hand
|
||||
sebserver.examconfig.props.label.zoomFeatureFlagRaiseHand.tooltip=
|
||||
sebserver.examconfig.props.label.zoomFeatureFlagRaiseHand.tooltip=Allows SEB users to use Zoom's raise hand function in SEB during a broadcast or chat
|
||||
sebserver.examconfig.props.label.zoomFeatureFlagRecording=Allow Recording
|
||||
sebserver.examconfig.props.label.zoomFeatureFlagRecording.tooltip=
|
||||
sebserver.examconfig.props.label.zoomFeatureFlagTileView=Allow Tile View
|
||||
sebserver.examconfig.props.label.zoomFeatureFlagTileView.tooltip=Note: Disabling Allow Tile View is not yet functional in this version.
|
||||
sebserver.examconfig.props.label.zoomFeatureFlagTileView.tooltip=Not yet implemented
|
||||
sebserver.examconfig.props.label.zoomRoom=Room
|
||||
sebserver.examconfig.props.label.zoomRoom.tooltip=
|
||||
sebserver.examconfig.props.label.zoomServerURL=Server URL
|
||||
|
@ -1480,16 +1482,16 @@ sebserver.examconfig.props.label.zoomUserInfoDisplayName.tooltip=
|
|||
sebserver.examconfig.props.label.zoomUserInfoEMail=Info Mail
|
||||
sebserver.examconfig.props.label.zoomUserInfoEMail.tooltip=
|
||||
sebserver.examconfig.props.label.zoomVideoMuted=Video Initially Muted
|
||||
sebserver.examconfig.props.label.zoomVideoMuted.tooltip=
|
||||
sebserver.examconfig.props.label.zoomVideoMuted.tooltip=If this option is activated, a video broadcasts or chat starts with SEB clients cameras off.<br/>The SEB users then can start/stop their video individually using the respective button in the Zoom windows in SEB
|
||||
|
||||
sebserver.examconfig.props.label.zoomReceiveAudio=Receive Audio
|
||||
sebserver.examconfig.props.label.zoomReceiveAudio.tooltip=Global settings for receiving and sending audio/video streams.<br/>Streams can also be enabled/disabled by SEB Server during the exam session.
|
||||
sebserver.examconfig.props.label.zoomReceiveAudio.tooltip=Not yet implemented
|
||||
sebserver.examconfig.props.label.zoomReceiveVideo=Receive Video
|
||||
sebserver.examconfig.props.label.zoomReceiveVideo.tooltip=Global settings for receiving and sending audio/video streams.<br/>Streams can also be enabled/disabled by SEB Server during the exam session.
|
||||
sebserver.examconfig.props.label.zoomReceiveVideo.tooltip=Controls video connection if Proctoring View Display Policy allows initial display of Zoom window on SEB startup
|
||||
sebserver.examconfig.props.label.zoomSendAudio=Send Audio
|
||||
sebserver.examconfig.props.label.zoomSendAudio.tooltip=Global settings for receiving and sending audio/video streams.<br/>Streams can also be enabled/disabled by SEB Server during the exam session.
|
||||
sebserver.examconfig.props.label.zoomSendAudio.tooltip=Not yet implemented
|
||||
sebserver.examconfig.props.label.zoomSendVideo=Send Video
|
||||
sebserver.examconfig.props.label.zoomSendVideo.tooltip=Global settings for receiving and sending audio/video streams.<br/>Streams can also be enabled/disabled by SEB Server during the exam session.
|
||||
sebserver.examconfig.props.label.zoomSendVideo.tooltip=Not yet implemented
|
||||
|
||||
|
||||
sebserver.examconfig.props.label.showProctoringViewButton=Show Proctoring Button
|
||||
|
|
|
@ -38,7 +38,6 @@ import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientConnectionRe
|
|||
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientEventRecord;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.ClientConnectionDataInternal;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.ExamSessionCacheService;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.indicator.AbstractPingIndicator;
|
||||
|
||||
@Sql(scripts = { "classpath:schema-test.sql", "classpath:data-test.sql", "classpath:data-test-additional.sql" })
|
||||
public class SebConnectionTest extends ExamAPIIntegrationTester {
|
||||
|
@ -468,20 +467,15 @@ public class SebConnectionTest extends ExamAPIIntegrationTester {
|
|||
assertFalse(ccdi.indicatorValues.isEmpty());
|
||||
final IndicatorValue pingIndicator = ccdi.indicatorValues.iterator().next();
|
||||
assertTrue(pingIndicator.getType() == IndicatorType.LAST_PING);
|
||||
assertEquals("0", String.valueOf(((AbstractPingIndicator) pingIndicator).getPingNumber()));
|
||||
|
||||
super.sendPing(accessToken, connectionToken, 1);
|
||||
Thread.sleep(200);
|
||||
assertEquals("1", String.valueOf(((AbstractPingIndicator) pingIndicator).getPingNumber()));
|
||||
super.sendPing(accessToken, connectionToken, 2);
|
||||
Thread.sleep(200);
|
||||
assertEquals("2", String.valueOf(((AbstractPingIndicator) pingIndicator).getPingNumber()));
|
||||
super.sendPing(accessToken, connectionToken, 3);
|
||||
Thread.sleep(200);
|
||||
assertEquals("3", String.valueOf(((AbstractPingIndicator) pingIndicator).getPingNumber()));
|
||||
super.sendPing(accessToken, connectionToken, 5);
|
||||
Thread.sleep(200);
|
||||
assertEquals("5", String.valueOf(((AbstractPingIndicator) pingIndicator).getPingNumber()));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -531,7 +525,6 @@ public class SebConnectionTest extends ExamAPIIntegrationTester {
|
|||
assertFalse(ccdi.indicatorValues.isEmpty());
|
||||
final IndicatorValue pingIndicator = ccdi.indicatorValues.iterator().next();
|
||||
assertTrue(pingIndicator.getType() == IndicatorType.LAST_PING);
|
||||
assertEquals("0", String.valueOf(((AbstractPingIndicator) pingIndicator).getPingNumber()));
|
||||
|
||||
MockHttpServletResponse sendEvent = super.sendEvent(
|
||||
accessToken,
|
||||
|
|
|
@ -18,7 +18,7 @@ import org.mockito.Mockito;
|
|||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
|
||||
import ch.ethz.seb.sebserver.webservice.datalayer.batis.ClientEventExtensionMapper;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientEventDAO;
|
||||
|
||||
public class PingIntervalClientIndicatorTest {
|
||||
|
||||
|
@ -32,10 +32,10 @@ public class PingIntervalClientIndicatorTest {
|
|||
|
||||
DateTimeUtils.setCurrentMillisProvider(() -> 1L);
|
||||
|
||||
final ClientEventExtensionMapper clientEventExtensionMapper = Mockito.mock(ClientEventExtensionMapper.class);
|
||||
final ClientEventDAO clientEventDAO = Mockito.mock(ClientEventDAO.class);
|
||||
|
||||
final PingIntervalClientIndicator pingIntervalClientIndicator =
|
||||
new PingIntervalClientIndicator(clientEventExtensionMapper, null);
|
||||
new PingIntervalClientIndicator(clientEventDAO);
|
||||
assertEquals("0.0", String.valueOf(pingIntervalClientIndicator.getValue()));
|
||||
}
|
||||
|
||||
|
@ -44,10 +44,10 @@ public class PingIntervalClientIndicatorTest {
|
|||
|
||||
DateTimeUtils.setCurrentMillisProvider(() -> 1L);
|
||||
|
||||
final ClientEventExtensionMapper clientEventExtensionMapper = Mockito.mock(ClientEventExtensionMapper.class);
|
||||
final ClientEventDAO clientEventDAO = Mockito.mock(ClientEventDAO.class);
|
||||
|
||||
final PingIntervalClientIndicator pingIntervalClientIndicator =
|
||||
new PingIntervalClientIndicator(clientEventExtensionMapper, null);
|
||||
new PingIntervalClientIndicator(clientEventDAO);
|
||||
assertEquals("0.0", String.valueOf(pingIntervalClientIndicator.getValue()));
|
||||
|
||||
DateTimeUtils.setCurrentMillisProvider(() -> 10L);
|
||||
|
@ -59,10 +59,10 @@ public class PingIntervalClientIndicatorTest {
|
|||
public void testSerialization() throws JsonProcessingException {
|
||||
DateTimeUtils.setCurrentMillisProvider(() -> 1L);
|
||||
|
||||
final ClientEventExtensionMapper clientEventExtensionMapper = Mockito.mock(ClientEventExtensionMapper.class);
|
||||
final ClientEventDAO clientEventDAO = Mockito.mock(ClientEventDAO.class);
|
||||
|
||||
final PingIntervalClientIndicator pingIntervalClientIndicator =
|
||||
new PingIntervalClientIndicator(clientEventExtensionMapper, null);
|
||||
new PingIntervalClientIndicator(clientEventDAO);
|
||||
final JSONMapper jsonMapper = new JSONMapper();
|
||||
final String json = jsonMapper.writeValueAsString(pingIntervalClientIndicator);
|
||||
assertEquals("{\"indicatorValue\":0.0,\"indicatorType\":\"LAST_PING\"}", json);
|
||||
|
|
Loading…
Add table
Reference in a new issue