From d3a372cfacc8278daa969fc2fec0c4bb6cbf4c2e Mon Sep 17 00:00:00 2001 From: anhefti Date: Tue, 24 Nov 2020 13:48:29 +0100 Subject: [PATCH] SEBSERV-136 SEBSERV-147 implementation and integration tests --- .../ch/ethz/seb/sebserver/gbl/api/API.java | 1 + .../model/session/ClientConnectionData.java | 8 + .../content/MonitoringClientConnection.java | 2 +- .../gui/content/MonitoringRunningExam.java | 2 +- .../api/session/DisableClientConnection.java | 2 +- .../api/session/GetClientConnectionData.java | 2 +- .../session/GetClientConnectionDataList.java | 2 +- .../api/session/PropagateInstruction.java | 2 +- .../service/session/InstructionProcessor.java | 4 +- .../servicelayer/dao/ClientEventDAO.java | 5 + .../dao/impl/ClientEventDAOImpl.java | 39 +++++ .../PendingNotificationIndication.java | 16 ++ ....java => SEBClientInstructionService.java} | 2 +- .../session/SEBClientNotificationService.java | 48 ++++++ .../session/impl/AbstractClientIndicator.java | 16 +- .../impl/AbstractLogLevelCountIndicator.java | 49 ++++++ .../impl/ClientConnectionDataInternal.java | 20 ++- .../impl/ExamProctoringRoomServiceImpl.java | 6 +- .../session/impl/ExamSessionCacheService.java | 13 +- .../InternalClientConnectionDataFactory.java | 42 +++++ .../impl/SEBClientConnectionServiceImpl.java | 22 ++- ...a => SEBClientInstructionServiceImpl.java} | 8 +- .../SEBClientNotificationServiceImpl.java | 51 ++++++ .../api/ExamMonitoringController.java | 86 +++++++-- .../api/ExamProctoringController.java | 6 +- .../integration/UseCasesIntegrationTest.java | 20 +-- .../services/ClientEventServiceTest.java | 163 ++++++++++++++++++ src/test/resources/data-test-additional.sql | 9 +- 28 files changed, 569 insertions(+), 77 deletions(-) create mode 100644 src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/PendingNotificationIndication.java rename src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/{SEBInstructionService.java => SEBClientInstructionService.java} (96%) create mode 100644 src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/SEBClientNotificationService.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/InternalClientConnectionDataFactory.java rename src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/{SEBInstructionServiceImpl.java => SEBClientInstructionServiceImpl.java} (98%) create mode 100644 src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientNotificationServiceImpl.java create mode 100644 src/test/java/ch/ethz/seb/sebserver/webservice/integration/services/ClientEventServiceTest.java diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java b/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java index c9c68e65..c372e5a9 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java @@ -165,6 +165,7 @@ public final class API { public static final String EXAM_MONITORING_ENDPOINT = "/monitoring"; public static final String EXAM_MONITORING_INSTRUCTION_ENDPOINT = "/instruction"; + public static final String EXAM_MONITORING_NOTIFICATION_ENDPOINT = "/notification"; public static final String EXAM_MONITORING_DISABLE_CONNECTION_ENDPOINT = "/disable-connection"; public static final String EXAM_MONITORING_STATE_FILTER = "hidden-states"; public static final String EXAM_MONITORING_SEB_CONNECTION_TOKEN_PATH_SEGMENT = diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/session/ClientConnectionData.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/session/ClientConnectionData.java index 8bf90701..40a88b07 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/session/ClientConnectionData.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/session/ClientConnectionData.java @@ -14,15 +14,18 @@ import java.util.List; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import ch.ethz.seb.sebserver.gbl.util.Utils; +@JsonIgnoreProperties(ignoreUnknown = true) public class ClientConnectionData { public static final String ATTR_CLIENT_CONNECTION = "clientConnection"; public static final String ATTR_INDICATOR_VALUE = "indicatorValues"; public static final String ATTR_MISSING_PING = "missingPing"; + public static final String ATTR_PENDING_NOTIFICATION = "pendingNotification"; @JsonProperty(ATTR_CLIENT_CONNECTION) public final ClientConnection clientConnection; @@ -56,6 +59,11 @@ public class ClientConnectionData { return this.missingPing; } + @JsonProperty(ATTR_PENDING_NOTIFICATION) + public Boolean pendingNotification() { + return false; + } + @JsonIgnore public Long getConnectionId() { return this.clientConnection.id; diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/MonitoringClientConnection.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/MonitoringClientConnection.java index a269f12e..dc65b37a 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/MonitoringClientConnection.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/MonitoringClientConnection.java @@ -182,7 +182,7 @@ public class MonitoringClientConnection implements TemplateComposer { final RestCall.RestCallBuilder getConnectionData = restService.getBuilder(GetClientConnectionData.class) - .withURIVariable(API.PARAM_MODEL_ID, exam.getModelId()) + .withURIVariable(API.PARAM_PARENT_MODEL_ID, exam.getModelId()) .withURIVariable(API.EXAM_API_SEB_CONNECTION_TOKEN, connectionToken); final ClientConnectionData connectionData = getConnectionData diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/MonitoringRunningExam.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/MonitoringRunningExam.java index d150bf0e..1047f3c1 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/MonitoringRunningExam.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/MonitoringRunningExam.java @@ -181,7 +181,7 @@ public class MonitoringRunningExam implements TemplateComposer { final RestCall>.RestCallBuilder restCall = restService.getBuilder(GetClientConnectionDataList.class) - .withURIVariable(API.PARAM_MODEL_ID, exam.getModelId()); + .withURIVariable(API.PARAM_PARENT_MODEL_ID, exam.getModelId()); final ClientConnectionTable clientTable = new ClientConnectionTable( this.pageService, diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/DisableClientConnection.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/DisableClientConnection.java index abb446b7..9c32d523 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/DisableClientConnection.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/DisableClientConnection.java @@ -34,7 +34,7 @@ public class DisableClientConnection extends RestCall { HttpMethod.POST, MediaType.APPLICATION_FORM_URLENCODED, API.EXAM_MONITORING_ENDPOINT - + API.MODEL_ID_VAR_PATH_SEGMENT + + API.PARENT_MODEL_ID_VAR_PATH_SEGMENT + API.EXAM_MONITORING_DISABLE_CONNECTION_ENDPOINT); } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/GetClientConnectionData.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/GetClientConnectionData.java index 7987aa4f..57de53ad 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/GetClientConnectionData.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/GetClientConnectionData.java @@ -35,7 +35,7 @@ public class GetClientConnectionData extends RestCall { HttpMethod.GET, MediaType.APPLICATION_FORM_URLENCODED, API.EXAM_MONITORING_ENDPOINT + - API.MODEL_ID_VAR_PATH_SEGMENT + + API.PARENT_MODEL_ID_VAR_PATH_SEGMENT + API.EXAM_MONITORING_SEB_CONNECTION_TOKEN_PATH_SEGMENT); } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/GetClientConnectionDataList.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/GetClientConnectionDataList.java index c4e99b85..acf05394 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/GetClientConnectionDataList.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/GetClientConnectionDataList.java @@ -37,7 +37,7 @@ public class GetClientConnectionDataList extends RestCall { HttpMethod.POST, MediaType.APPLICATION_JSON_UTF8, API.EXAM_MONITORING_ENDPOINT - + API.MODEL_ID_VAR_PATH_SEGMENT + + API.PARENT_MODEL_ID_VAR_PATH_SEGMENT + API.EXAM_MONITORING_INSTRUCTION_ENDPOINT); } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/session/InstructionProcessor.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/InstructionProcessor.java index 9a409f66..3103e004 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/session/InstructionProcessor.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/InstructionProcessor.java @@ -93,7 +93,7 @@ public class InstructionProcessor { null); processInstruction(() -> this.restService.getBuilder(PropagateInstruction.class) - .withURIVariable(API.PARAM_MODEL_ID, String.valueOf(examId)) + .withURIVariable(API.PARAM_PARENT_MODEL_ID, String.valueOf(examId)) .withBody(clientInstruction) .call() .getOrThrow(), @@ -124,7 +124,7 @@ public class InstructionProcessor { } processInstruction(() -> this.restService.getBuilder(DisableClientConnection.class) - .withURIVariable(API.PARAM_MODEL_ID, String.valueOf(examId)) + .withURIVariable(API.PARAM_PARENT_MODEL_ID, String.valueOf(examId)) .withFormParam( Domain.CLIENT_CONNECTION.ATTR_CONNECTION_TOKEN, StringUtils.join(connectionTokens, Constants.LIST_SEPARATOR)) diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ClientEventDAO.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ClientEventDAO.java index f913def5..1db737fc 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ClientEventDAO.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ClientEventDAO.java @@ -9,6 +9,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.dao; import java.util.Collection; +import java.util.List; import java.util.function.Predicate; import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent; @@ -26,4 +27,8 @@ public interface ClientEventDAO extends EntityDAO { FilterMap filterMap, Predicate predicate); + Result> getPendingNotifications(Long clientConnectionId); + + Result confirmPendingNotification(Long notificationId, Long clientConnectionId); + } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ClientEventDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ClientEventDAOImpl.java index 75b02f07..951facaa 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ClientEventDAOImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ClientEventDAOImpl.java @@ -13,6 +13,7 @@ import static org.mybatis.dynamic.sql.SqlBuilder.*; 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; @@ -31,6 +32,7 @@ import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.EventType; import ch.ethz.seb.sebserver.gbl.model.session.ExtendedClientEvent; 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.ClientEventExtensionMapper; import ch.ethz.seb.sebserver.webservice.datalayer.batis.ClientEventExtensionMapper.ConnectionEventJoinRecord; import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientConnectionRecordDynamicSqlSupport; @@ -183,6 +185,43 @@ public class ClientEventDAOImpl implements ClientEventDAO { .collect(Collectors.toList())); } + @Override + @Transactional(readOnly = true) + public Result> getPendingNotifications(final Long clientConnectionId) { + return Result.tryCatch(() -> this.clientEventRecordMapper.selectByExample() + .where(ClientEventRecordDynamicSqlSupport.clientConnectionId, isEqualTo(clientConnectionId)) + .and(ClientEventRecordDynamicSqlSupport.type, isEqualTo(EventType.NOTIFICATION.id)) + .build() + .execute() + .stream() + .map(ClientEventDAOImpl::toDomainModel) + .flatMap(DAOLoggingSupport::logAndSkipOnError) + .collect(Collectors.toList())); + } + + @Override + @Transactional + public Result confirmPendingNotification(final Long notificationId, final Long clientConnectionId) { + return Result.tryCatch(() -> { + final Long pk = this.clientEventRecordMapper.selectIdsByExample() + .where(ClientEventRecordDynamicSqlSupport.id, isEqualTo(notificationId)) + .and(ClientEventRecordDynamicSqlSupport.type, isEqualTo(EventType.NOTIFICATION.id)) + .build() + .execute() + .stream().collect(Utils.toSingleton()); + + this.clientEventRecordMapper.updateByPrimaryKeySelective(new ClientEventRecord( + pk, + null, + EventType.NOTIFICATION_CONFIRMED.id, + null, null, null, null)); + + return this.clientEventRecordMapper.selectByPrimaryKey(pk); + }) + .flatMap(ClientEventDAOImpl::toDomainModel) + .onError(TransactionHandler::rollback); + } + @Override @Transactional public Result createNew(final ClientEvent data) { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/PendingNotificationIndication.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/PendingNotificationIndication.java new file mode 100644 index 00000000..73af0efd --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/PendingNotificationIndication.java @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2020 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; + +@FunctionalInterface +public interface PendingNotificationIndication { + + boolean notifictionPending(); + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/SEBInstructionService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/SEBClientInstructionService.java similarity index 96% rename from src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/SEBInstructionService.java rename to src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/SEBClientInstructionService.java index e4cb9a29..a7a40ef8 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/SEBInstructionService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/SEBClientInstructionService.java @@ -25,7 +25,7 @@ import ch.ethz.seb.sebserver.webservice.WebserviceInfo; * * SEB instructions are sent as response of a SEB Ping on a active SEB Connection * If there is an instruction in the queue for a specified SEB Client. */ -public interface SEBInstructionService { +public interface SEBClientInstructionService { /** Get the underling WebserviceInfo * diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/SEBClientNotificationService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/SEBClientNotificationService.java new file mode 100644 index 00000000..b0fe860d --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/SEBClientNotificationService.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2020 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; + +import java.util.List; + +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; + +import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent; +import ch.ethz.seb.sebserver.gbl.util.Result; + +/** Service to maintain SEB Client notifications. */ +public interface SEBClientNotificationService { + + public static final String CACHE_CLIENT_NOTIFICATION = "LIENT_NOTIFICATION_CACHE"; + + /** Indicates whether the client connection with the specified identifier has any + * pending notification or not. Pending means a non-confirmed notification + * + * @param clientConnectionId the client connection identifier + * @return true if there is any pending notification for the specified client connection */ + @Cacheable( + cacheNames = CACHE_CLIENT_NOTIFICATION, + key = "#clientConnectionId", + condition = "#result != null && #result") + Boolean hasAnyPendingNotification(Long clientConnectionId); + + Result> getPendingNotifications(Long clientConnectionId); + + @CacheEvict( + cacheNames = CACHE_CLIENT_NOTIFICATION, + key = "#clientConnectionId") + Result confirmPendingNotification(Long notificationId, final Long clientConnectionId); + + @CacheEvict( + cacheNames = CACHE_CLIENT_NOTIFICATION, + key = "#clientConnectionId") + default void notifyNewNotification(final Long clientConnectionId) { + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/AbstractClientIndicator.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/AbstractClientIndicator.java index 32219e11..6a2951d1 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/AbstractClientIndicator.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/AbstractClientIndicator.java @@ -8,11 +8,9 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl; -import org.apache.commons.lang3.StringUtils; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; -import ch.ethz.seb.sebserver.gbl.Constants; import ch.ethz.seb.sebserver.gbl.model.exam.Indicator; import ch.ethz.seb.sebserver.webservice.servicelayer.session.ClientIndicator; @@ -21,7 +19,6 @@ public abstract class AbstractClientIndicator implements ClientIndicator { protected Long examId; protected Long connectionId; protected boolean cachingEnabled; - protected String[] tags; protected double currentValue = Double.NaN; @@ -34,15 +31,6 @@ public abstract class AbstractClientIndicator implements ClientIndicator { this.examId = (indicatorDefinition != null) ? indicatorDefinition.examId : null; this.connectionId = connectionId; this.cachingEnabled = cachingEnabled; - if (indicatorDefinition == null || indicatorDefinition.tags == null) { - this.tags = null; - } else { - this.tags = StringUtils.split(indicatorDefinition.tags, Constants.COMMA); - for (int i = 0; i < this.tags.length; i++) { - this.tags[i] = Constants.ANGLE_BRACE_OPEN + this.tags[i] + Constants.ANGLE_BRACE_CLOSE; - } - } - } @Override @@ -55,6 +43,10 @@ public abstract class AbstractClientIndicator implements ClientIndicator { return this.connectionId; } + public void reset() { + this.currentValue = Double.NaN; + } + @Override public double getValue() { if (Double.isNaN(this.currentValue) || !this.cachingEnabled) { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/AbstractLogLevelCountIndicator.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/AbstractLogLevelCountIndicator.java index 8d2025fc..bd1cf7bc 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/AbstractLogLevelCountIndicator.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/AbstractLogLevelCountIndicator.java @@ -17,6 +17,12 @@ import java.util.List; import java.util.Set; import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; +import org.mybatis.dynamic.sql.SqlBuilder; +import org.mybatis.dynamic.sql.SqlCriterion; + +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; import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.EventType; import ch.ethz.seb.sebserver.gbl.util.Utils; @@ -28,6 +34,7 @@ public abstract class AbstractLogLevelCountIndicator extends AbstractClientIndic private final Set observed; private final List eventTypeIds; private final ClientEventRecordMapper clientEventRecordMapper; + protected String[] tags; protected AbstractLogLevelCountIndicator( final ClientEventRecordMapper clientEventRecordMapper, @@ -38,6 +45,20 @@ public abstract class AbstractLogLevelCountIndicator extends AbstractClientIndic this.eventTypeIds = Utils.immutableListOf(Arrays.stream(eventTypes) .map(et -> et.id) .collect(Collectors.toList())); + + } + + @Override + public void init(final Indicator indicatorDefinition, final Long connectionId, final boolean cachingEnabled) { + super.init(indicatorDefinition, connectionId, cachingEnabled); + if (indicatorDefinition == null || indicatorDefinition.tags == null) { + this.tags = null; + } else { + this.tags = StringUtils.split(indicatorDefinition.tags, Constants.COMMA); + for (int i = 0; i < this.tags.length; i++) { + this.tags[i] = Constants.ANGLE_BRACE_OPEN + this.tags[i] + Constants.ANGLE_BRACE_CLOSE; + } + } } @Override @@ -47,12 +68,40 @@ public abstract class AbstractLogLevelCountIndicator extends AbstractClientIndic .where(ClientEventRecordDynamicSqlSupport.clientConnectionId, isEqualTo(this.connectionId)) .and(ClientEventRecordDynamicSqlSupport.type, isIn(this.eventTypeIds)) .and(ClientEventRecordDynamicSqlSupport.serverTime, isLessThan(timestamp)) + .and( + ClientEventRecordDynamicSqlSupport.text, + isLikeWhenPresent(getfirstTagSQL()), + getSubTagSQL()) .build() .execute(); return errors.doubleValue(); } + private String getfirstTagSQL() { + if (this.tags == null || this.tags.length == 0) { + return null; + } + + return Utils.toSQLWildcard(this.tags[0]); + } + + @SuppressWarnings("unchecked") + private SqlCriterion[] getSubTagSQL() { + if (this.tags == null || this.tags.length == 0 || this.tags.length == 1) { + return new SqlCriterion[0]; + } + + final SqlCriterion[] result = new SqlCriterion[this.tags.length - 1]; + for (int i = 1; i < this.tags.length; i++) { + result[i - 1] = SqlBuilder.or( + ClientEventRecordDynamicSqlSupport.text, + isLike(Utils.toSQLWildcard(this.tags[1]))); + } + + return result; + } + @Override public void notifyValueChange(final ClientEvent event) { if (this.tags == null || this.tags.length == 0) { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ClientConnectionDataInternal.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ClientConnectionDataInternal.java index 791cadf2..24e3ad9d 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ClientConnectionDataInternal.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ClientConnectionDataInternal.java @@ -23,6 +23,7 @@ import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData; import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.EventType; import ch.ethz.seb.sebserver.webservice.servicelayer.session.ClientIndicator; +import ch.ethz.seb.sebserver.webservice.servicelayer.session.PendingNotificationIndication; public class ClientConnectionDataInternal extends ClientConnectionData { @@ -31,12 +32,15 @@ public class ClientConnectionDataInternal extends ClientConnectionData { final EnumMap> indicatorMapping; PingIntervalClientIndicator pingIndicator = null; + private final PendingNotificationIndication pendingNotificationIndication; protected ClientConnectionDataInternal( final ClientConnection clientConnection, + final PendingNotificationIndication pendingNotificationIndication, final List clientIndicators) { super(clientConnection, clientIndicators); + this.pendingNotificationIndication = pendingNotificationIndication; this.indicatorMapping = new EnumMap<>(EventType.class); for (final ClientIndicator clientIndicator : clientIndicators) { @@ -62,17 +66,21 @@ public class ClientConnectionDataInternal extends ClientConnectionData { } Collection getIndicatorMapping(final EventType eventType) { - if (!this.indicatorMapping.containsKey(eventType)) { - return Collections.emptyList(); - } - - return this.indicatorMapping.get(eventType); + return this.indicatorMapping.getOrDefault( + eventType, + Collections.emptyList()); } @Override - @JsonProperty("missingPing") + @JsonProperty(ATTR_MISSING_PING) public Boolean getMissingPing() { return this.pingIndicator.missingPing; } + @Override + @JsonProperty(ATTR_PENDING_NOTIFICATION) + public Boolean pendingNotification() { + return this.pendingNotificationIndication.notifictionPending(); + } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamProctoringRoomServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamProctoringRoomServiceImpl.java index cc39e92f..3ee5c152 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamProctoringRoomServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamProctoringRoomServiceImpl.java @@ -34,7 +34,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.RemoteProctoringRoomDAO import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamAdminService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamProctoringRoomService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService; -import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBInstructionService; +import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientInstructionService; @Lazy @Service @@ -45,14 +45,14 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService private final RemoteProctoringRoomDAO remoteProctoringRoomDAO; private final ClientConnectionDAO clientConnectionDAO; - private final SEBInstructionService sebInstructionService; + private final SEBClientInstructionService sebInstructionService; private final ExamAdminService examAdminService; private final ExamSessionService examSessionService; public ExamProctoringRoomServiceImpl( final RemoteProctoringRoomDAO remoteProctoringRoomDAO, final ClientConnectionDAO clientConnectionDAO, - final SEBInstructionService sebInstructionService, + final SEBClientInstructionService sebInstructionService, final ExamAdminService examAdminService, final ExamSessionService examSessionService) { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionCacheService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionCacheService.java index 263ca352..72a0e368 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionCacheService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionCacheService.java @@ -54,7 +54,7 @@ public class ExamSessionCacheService { private final ExamDAO examDAO; private final ClientConnectionDAO clientConnectionDAO; - private final ClientIndicatorFactory clientIndicatorFactory; + private final InternalClientConnectionDataFactory internalClientConnectionDataFactory; private final ExamConfigService sebExamConfigService; private final ClientEventRecordMapper clientEventRecordMapper; private final ExamUpdateHandler examUpdateHandler; @@ -62,7 +62,7 @@ public class ExamSessionCacheService { protected ExamSessionCacheService( final ExamDAO examDAO, final ClientConnectionDAO clientConnectionDAO, - final ClientIndicatorFactory clientIndicatorFactory, + final InternalClientConnectionDataFactory internalClientConnectionDataFactory, final ExamConfigService sebExamConfigService, final ClientEventRecordMapper clientEventRecordMapper, final ExamUpdateHandler examUpdateHandler, @@ -70,7 +70,7 @@ public class ExamSessionCacheService { this.examDAO = examDAO; this.clientConnectionDAO = clientConnectionDAO; - this.clientIndicatorFactory = clientIndicatorFactory; + this.internalClientConnectionDataFactory = internalClientConnectionDataFactory; this.sebExamConfigService = sebExamConfigService; this.clientEventRecordMapper = clientEventRecordMapper; this.examUpdateHandler = examUpdateHandler; @@ -146,9 +146,7 @@ public class ExamSessionCacheService { if (clientConnection == null) { return null; } else { - return new ClientConnectionDataInternal( - clientConnection, - this.clientIndicatorFactory.createFor(clientConnection)); + return this.internalClientConnectionDataFactory.createClientConnectionData(clientConnection); } } @@ -239,7 +237,8 @@ public class ExamSessionCacheService { .byConnectionToken(connectionToken); if (result.hasError()) { - log.error("Failed to find/load ClientConnection with connectionToken {}", connectionToken, result.getError()); + log.error("Failed to find/load ClientConnection with connectionToken {}", connectionToken, + result.getError()); return null; } return result.get(); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/InternalClientConnectionDataFactory.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/InternalClientConnectionDataFactory.java new file mode 100644 index 00000000..38479cb4 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/InternalClientConnectionDataFactory.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2020 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.model.session.ClientConnection; +import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; +import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientNotificationService; + +@Lazy +@Service +@WebServiceProfile +public class InternalClientConnectionDataFactory { + + private final ClientIndicatorFactory clientIndicatorFactory; + private final SEBClientNotificationService sebClientNotificationService; + + public InternalClientConnectionDataFactory( + final ClientIndicatorFactory clientIndicatorFactory, + final SEBClientNotificationService sebClientNotificationService) { + + this.clientIndicatorFactory = clientIndicatorFactory; + this.sebClientNotificationService = sebClientNotificationService; + } + + public ClientConnectionDataInternal createClientConnectionData(final ClientConnection clientConnection) { + return new ClientConnectionDataInternal( + clientConnection, + () -> this.sebClientNotificationService + .hasAnyPendingNotification(clientConnection.id), + this.clientIndicatorFactory.createFor(clientConnection)); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientConnectionServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientConnectionServiceImpl.java index 0b23aeaf..72e717e3 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientConnectionServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientConnectionServiceImpl.java @@ -28,6 +28,7 @@ import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData; 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; @@ -39,7 +40,8 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.session.EventHandlingStrate 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.SEBInstructionService; +import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientNotificationService; +import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientInstructionService; import ch.ethz.seb.sebserver.webservice.weblayer.api.APIConstraintViolationException; @Lazy @@ -63,7 +65,8 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic private final ClientConnectionDAO clientConnectionDAO; private final PingHandlingStrategy pingHandlingStrategy; private final SEBClientConfigDAO sebClientConfigDAO; - private final SEBInstructionService sebInstructionService; + private final SEBClientInstructionService sebInstructionService; + private final SEBClientNotificationService sebClientNotificationService; private final WebserviceInfo webserviceInfo; private final ExamAdminService examAdminService; @@ -72,7 +75,8 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic final EventHandlingStrategyFactory eventHandlingStrategyFactory, final PingHandlingStrategyFactory pingHandlingStrategyFactory, final SEBClientConfigDAO sebClientConfigDAO, - final SEBInstructionService sebInstructionService, + final SEBClientInstructionService sebInstructionService, + final SEBClientNotificationService sebClientNotificationService, final ExamAdminService examAdminService) { this.examSessionService = examSessionService; @@ -83,6 +87,7 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic this.eventHandlingStrategy = eventHandlingStrategyFactory.get(); this.sebClientConfigDAO = sebClientConfigDAO; this.sebInstructionService = sebInstructionService; + this.sebClientNotificationService = sebClientNotificationService; this.webserviceInfo = sebInstructionService.getWebserviceInfo(); this.examAdminService = examAdminService; } @@ -519,9 +524,14 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic event, activeClientConnection.getConnectionId())); - // update indicators - activeClientConnection.getIndicatorMapping(event.eventType) - .forEach(indicator -> indicator.notifyValueChange(event)); + if (event.eventType == EventType.NOTIFICATION || event.eventType == EventType.NOTIFICATION_CONFIRMED) { + // notify notification service + this.sebClientNotificationService.notifyNewNotification(activeClientConnection.getConnectionId()); + } else { + // update indicators + activeClientConnection.getIndicatorMapping(event.eventType) + .forEach(indicator -> indicator.notifyValueChange(event)); + } } else { log.warn("No active ClientConnection found for connectionToken: {}", connectionToken); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBInstructionServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientInstructionServiceImpl.java similarity index 98% rename from src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBInstructionServiceImpl.java rename to src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientInstructionServiceImpl.java index f1d284fe..a8e773c8 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBInstructionServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientInstructionServiceImpl.java @@ -35,14 +35,14 @@ import ch.ethz.seb.sebserver.webservice.WebserviceInfo; import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientInstructionRecord; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientConnectionDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientInstructionDAO; -import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBInstructionService; +import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientInstructionService; @Lazy @Service @WebServiceProfile -public class SEBInstructionServiceImpl implements SEBInstructionService { +public class SEBClientInstructionServiceImpl implements SEBClientInstructionService { - private static final Logger log = LoggerFactory.getLogger(SEBInstructionServiceImpl.class); + private static final Logger log = LoggerFactory.getLogger(SEBClientInstructionServiceImpl.class); private static final int INSTRUCTION_QUEUE_MAX_SIZE = 10; private static final String JSON_INST = "instruction"; @@ -57,7 +57,7 @@ public class SEBInstructionServiceImpl implements SEBInstructionService { private long lastRefresh = 0; - public SEBInstructionServiceImpl( + public SEBClientInstructionServiceImpl( final WebserviceInfo webserviceInfo, final ClientConnectionDAO clientConnectionDAO, final ClientInstructionDAO clientInstructionDAO, diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientNotificationServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientNotificationServiceImpl.java new file mode 100644 index 00000000..c13fbdd7 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientNotificationServiceImpl.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2020 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.util.Collections; +import java.util.List; + +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; + +import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent; +import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; +import ch.ethz.seb.sebserver.gbl.util.Result; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientEventDAO; +import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientNotificationService; + +@Lazy +@Service +@WebServiceProfile +public class SEBClientNotificationServiceImpl implements SEBClientNotificationService { + + private final ClientEventDAO clientEventDAO; + + public SEBClientNotificationServiceImpl(final ClientEventDAO clientEventDAO) { + this.clientEventDAO = clientEventDAO; + } + + @Override + public Boolean hasAnyPendingNotification(final Long clientConnectionId) { + return !getPendingNotifications(clientConnectionId) + .getOr(Collections.emptyList()) + .isEmpty(); + } + + @Override + public Result> getPendingNotifications(final Long clientConnectionId) { + return this.clientEventDAO.getPendingNotifications(clientConnectionId); + } + + @Override + public Result confirmPendingNotification(final Long notificatioId, final Long clientConnectionId) { + return this.clientEventDAO.confirmPendingNotification(notificatioId, clientConnectionId); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamMonitoringController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamMonitoringController.java index e4f143cc..c2d643cc 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamMonitoringController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamMonitoringController.java @@ -41,6 +41,7 @@ import ch.ethz.seb.sebserver.gbl.model.Page; import ch.ethz.seb.sebserver.gbl.model.exam.Exam; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData; +import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent; import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction; import ch.ethz.seb.sebserver.gbl.model.user.UserRole; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; @@ -51,7 +52,8 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.UserService; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientConnectionService; -import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBInstructionService; +import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientInstructionService; +import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientNotificationService; @WebServiceProfile @RestController @@ -62,21 +64,24 @@ public class ExamMonitoringController { private final SEBClientConnectionService sebClientConnectionService; private final ExamSessionService examSessionService; - private final SEBInstructionService sebInstructionService; + private final SEBClientInstructionService sebClientInstructionService; private final AuthorizationService authorization; private final PaginationService paginationService; + private final SEBClientNotificationService sebClientNotificationService; public ExamMonitoringController( final SEBClientConnectionService sebClientConnectionService, - final SEBInstructionService sebInstructionService, + final SEBClientInstructionService sebClientInstructionService, final AuthorizationService authorization, - final PaginationService paginationService) { + final PaginationService paginationService, + final SEBClientNotificationService sebClientNotificationService) { this.sebClientConnectionService = sebClientConnectionService; this.examSessionService = sebClientConnectionService.getExamSessionService(); - this.sebInstructionService = sebInstructionService; + this.sebClientInstructionService = sebClientInstructionService; this.authorization = authorization; this.paginationService = paginationService; + this.sebClientNotificationService = sebClientNotificationService; } /** This is called by Spring to initialize the WebDataBinder and is used here to @@ -150,7 +155,7 @@ public class ExamMonitoringController { } @RequestMapping( - path = API.MODEL_ID_VAR_PATH_SEGMENT, + path = API.PARENT_MODEL_ID_VAR_PATH_SEGMENT, method = RequestMethod.GET, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) @@ -159,7 +164,7 @@ public class ExamMonitoringController { name = API.PARAM_INSTITUTION_ID, required = true, defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId, - @PathVariable(name = API.PARAM_MODEL_ID, required = true) final Long examId, + @PathVariable(name = API.PARAM_PARENT_MODEL_ID, required = true) final Long examId, @RequestHeader(name = API.EXAM_MONITORING_STATE_FILTER, required = false) final String hiddenStates) { // check overall privilege @@ -194,7 +199,8 @@ public class ExamMonitoringController { } @RequestMapping( - path = API.MODEL_ID_VAR_PATH_SEGMENT + API.EXAM_MONITORING_SEB_CONNECTION_TOKEN_PATH_SEGMENT, + path = API.PARENT_MODEL_ID_VAR_PATH_SEGMENT + + API.EXAM_MONITORING_SEB_CONNECTION_TOKEN_PATH_SEGMENT, method = RequestMethod.GET, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) @@ -203,7 +209,7 @@ public class ExamMonitoringController { name = API.PARAM_INSTITUTION_ID, required = true, defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId, - @PathVariable(name = API.PARAM_MODEL_ID, required = true) final Long examId, + @PathVariable(name = API.PARAM_PARENT_MODEL_ID, required = true) final Long examId, @PathVariable(name = API.EXAM_API_SEB_CONNECTION_TOKEN, required = true) final String connectionToken) { // check overall privilege @@ -226,7 +232,8 @@ public class ExamMonitoringController { } @RequestMapping( - path = API.MODEL_ID_VAR_PATH_SEGMENT + API.EXAM_MONITORING_INSTRUCTION_ENDPOINT, + path = API.PARENT_MODEL_ID_VAR_PATH_SEGMENT + + API.EXAM_MONITORING_INSTRUCTION_ENDPOINT, method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_UTF8_VALUE) public void registerInstruction( @@ -234,14 +241,64 @@ public class ExamMonitoringController { name = API.PARAM_INSTITUTION_ID, required = true, defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId, - @PathVariable(name = API.PARAM_MODEL_ID, required = true) final Long examId, + @PathVariable(name = API.PARAM_PARENT_MODEL_ID, required = true) final Long examId, @Valid @RequestBody final ClientInstruction clientInstruction) { - this.sebInstructionService.registerInstruction(clientInstruction); + this.sebClientInstructionService.registerInstruction(clientInstruction); } @RequestMapping( - path = API.MODEL_ID_VAR_PATH_SEGMENT + API.EXAM_MONITORING_DISABLE_CONNECTION_ENDPOINT, + path = API.PARENT_MODEL_ID_VAR_PATH_SEGMENT + + API.EXAM_MONITORING_NOTIFICATION_ENDPOINT + + API.EXAM_MONITORING_SEB_CONNECTION_TOKEN_PATH_SEGMENT, + method = RequestMethod.GET, + consumes = MediaType.APPLICATION_JSON_UTF8_VALUE) + public Collection pendingNotifications( + @RequestParam( + name = API.PARAM_INSTITUTION_ID, + required = true, + defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId, + @PathVariable(name = API.PARAM_PARENT_MODEL_ID, required = true) final Long examId, + @PathVariable(name = API.EXAM_API_SEB_CONNECTION_TOKEN, required = true) final String connectionToken) { + + final ClientConnectionData connection = getConnectionDataForSingleConnection( + institutionId, + examId, + connectionToken); + return this.sebClientNotificationService + .getPendingNotifications(connection.getConnectionId()) + .getOrThrow(); + } + + @RequestMapping( + path = API.PARENT_MODEL_ID_VAR_PATH_SEGMENT + + API.EXAM_MONITORING_NOTIFICATION_ENDPOINT + + API.MODEL_ID_VAR_PATH_SEGMENT + + API.EXAM_MONITORING_SEB_CONNECTION_TOKEN_PATH_SEGMENT, + method = RequestMethod.POST, + consumes = MediaType.APPLICATION_JSON_UTF8_VALUE) + public void confirmNotification( + @RequestParam( + name = API.PARAM_INSTITUTION_ID, + required = true, + defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId, + @PathVariable(name = API.PARAM_PARENT_MODEL_ID, required = true) final Long examId, + @PathVariable(name = API.PARAM_MODEL_ID, required = true) final Long notificationId, + @PathVariable(name = API.EXAM_API_SEB_CONNECTION_TOKEN, required = true) final String connectionToken) { + + final ClientConnectionData connection = getConnectionDataForSingleConnection( + institutionId, + examId, + connectionToken); + this.sebClientNotificationService.confirmPendingNotification( + notificationId, + connection.getConnectionId()) + .getOrThrow(); + } + + @RequestMapping( + path = API.PARENT_MODEL_ID_VAR_PATH_SEGMENT + + API.EXAM_MONITORING_DISABLE_CONNECTION_ENDPOINT, method = RequestMethod.POST, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) public void disableConnection( @@ -249,7 +306,7 @@ public class ExamMonitoringController { name = API.PARAM_INSTITUTION_ID, required = true, defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId, - @PathVariable(name = API.PARAM_MODEL_ID, required = true) final Long examId, + @PathVariable(name = API.PARAM_PARENT_MODEL_ID, required = true) final Long examId, @RequestParam( name = Domain.CLIENT_CONNECTION.ATTR_CONNECTION_TOKEN, required = true) final String connectionToken) { @@ -266,7 +323,6 @@ public class ExamMonitoringController { .disableConnection(connectionToken, institutionId) .getOrThrow(); } - } private boolean hasRunningExamPrivilege(final Long examId, final Long institution) { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamProctoringController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamProctoringController.java index 2823c55b..a963b71a 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamProctoringController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamProctoringController.java @@ -44,7 +44,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamAdminService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamProctoringRoomService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamProctoringService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService; -import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBInstructionService; +import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientInstructionService; @WebServiceProfile @RestController @@ -55,14 +55,14 @@ public class ExamProctoringController { private final ExamProctoringRoomService examProcotringRoomService; private final ExamAdminService examAdminService; - private final SEBInstructionService sebInstructionService; + private final SEBClientInstructionService sebInstructionService; private final AuthorizationService authorization; private final ExamSessionService examSessionService; public ExamProctoringController( final ExamProctoringRoomService examProcotringRoomService, final ExamAdminService examAdminService, - final SEBInstructionService sebInstructionService, + final SEBClientInstructionService sebInstructionService, final AuthorizationService authorization, final ExamSessionService examSessionService) { diff --git a/src/test/java/ch/ethz/seb/sebserver/gui/integration/UseCasesIntegrationTest.java b/src/test/java/ch/ethz/seb/sebserver/gui/integration/UseCasesIntegrationTest.java index d6ee0a6d..34aed470 100644 --- a/src/test/java/ch/ethz/seb/sebserver/gui/integration/UseCasesIntegrationTest.java +++ b/src/test/java/ch/ethz/seb/sebserver/gui/integration/UseCasesIntegrationTest.java @@ -918,8 +918,8 @@ public class UseCasesIntegrationTest extends GuiIntegrationTest { final Result newIndicatorResult = restService .getBuilder(NewIndicator.class) .withFormParam(Domain.INDICATOR.ATTR_EXAM_ID, exam.getModelId()) - .withFormParam(Domain.INDICATOR.ATTR_NAME, "Ping") - .withFormParam(Domain.INDICATOR.ATTR_TYPE, IndicatorType.LAST_PING.name) + .withFormParam(Domain.INDICATOR.ATTR_NAME, "Errors") + .withFormParam(Domain.INDICATOR.ATTR_TYPE, IndicatorType.ERROR_COUNT.name) .withFormParam(Domain.INDICATOR.ATTR_COLOR, "000001") .call(); @@ -927,7 +927,7 @@ public class UseCasesIntegrationTest extends GuiIntegrationTest { assertFalse(newIndicatorResult.hasError()); final Indicator newIndicator = newIndicatorResult.get(); - assertEquals("Ping", newIndicator.name); + assertEquals("Errors", newIndicator.name); assertEquals("000001", newIndicator.defaultColor); final Indicator indicatorToSave = new Indicator( @@ -951,7 +951,7 @@ public class UseCasesIntegrationTest extends GuiIntegrationTest { assertFalse(savedIndicatorResult.hasError()); final Indicator savedIndicator = savedIndicatorResult.get(); - assertEquals("Ping", savedIndicator.name); + assertEquals("Errors", savedIndicator.name); assertEquals("000001", savedIndicator.defaultColor); final Collection thresholds = savedIndicator.getThresholds(); assertFalse(thresholds.isEmpty()); @@ -2089,7 +2089,7 @@ public class UseCasesIntegrationTest extends GuiIntegrationTest { // get SEB connections Result> connectionsCall = restService.getBuilder(GetClientConnectionDataList.class) - .withURIVariable(API.PARAM_MODEL_ID, exam.getModelId()) + .withURIVariable(API.PARAM_PARENT_MODEL_ID, exam.getModelId()) .call(); assertNotNull(connectionsCall); @@ -2122,7 +2122,7 @@ public class UseCasesIntegrationTest extends GuiIntegrationTest { // send quit instruction connectionsCall = restService.getBuilder(GetClientConnectionDataList.class) - .withURIVariable(API.PARAM_MODEL_ID, exam.getModelId()) + .withURIVariable(API.PARAM_PARENT_MODEL_ID, exam.getModelId()) .call(); assertNotNull(connectionsCall); @@ -2141,7 +2141,7 @@ public class UseCasesIntegrationTest extends GuiIntegrationTest { null); final Result instructionCall = restService.getBuilder(PropagateInstruction.class) - .withURIVariable(API.PARAM_MODEL_ID, String.valueOf(exam.id)) + .withURIVariable(API.PARAM_PARENT_MODEL_ID, String.valueOf(exam.id)) .withBody(clientInstruction) .call(); @@ -2155,7 +2155,7 @@ public class UseCasesIntegrationTest extends GuiIntegrationTest { connectionsCall = restService.getBuilder(GetClientConnectionDataList.class) - .withURIVariable(API.PARAM_MODEL_ID, exam.getModelId()) + .withURIVariable(API.PARAM_PARENT_MODEL_ID, exam.getModelId()) .call(); assertNotNull(connectionsCall); @@ -2171,7 +2171,7 @@ public class UseCasesIntegrationTest extends GuiIntegrationTest { // disable connection final Result disableCall = restService.getBuilder(DisableClientConnection.class) - .withURIVariable(API.PARAM_MODEL_ID, exam.getModelId()) + .withURIVariable(API.PARAM_PARENT_MODEL_ID, exam.getModelId()) .withFormParam( Domain.CLIENT_CONNECTION.ATTR_CONNECTION_TOKEN, conData.clientConnection.connectionToken) @@ -2180,7 +2180,7 @@ public class UseCasesIntegrationTest extends GuiIntegrationTest { assertFalse(disableCall.hasError()); connectionsCall = restService.getBuilder(GetClientConnectionDataList.class) - .withURIVariable(API.PARAM_MODEL_ID, exam.getModelId()) + .withURIVariable(API.PARAM_PARENT_MODEL_ID, exam.getModelId()) .call(); assertNotNull(connectionsCall); diff --git a/src/test/java/ch/ethz/seb/sebserver/webservice/integration/services/ClientEventServiceTest.java b/src/test/java/ch/ethz/seb/sebserver/webservice/integration/services/ClientEventServiceTest.java new file mode 100644 index 00000000..2fced3a3 --- /dev/null +++ b/src/test/java/ch/ethz/seb/sebserver/webservice/integration/services/ClientEventServiceTest.java @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2020 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.integration.services; + +import static org.junit.Assert.*; + +import java.util.Collection; +import java.util.Optional; + +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.jdbc.Sql; + +import ch.ethz.seb.sebserver.gbl.model.exam.Indicator.IndicatorType; +import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; +import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus; +import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData; +import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent; +import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.EventType; +import ch.ethz.seb.sebserver.gbl.model.session.IndicatorValue; +import ch.ethz.seb.sebserver.webservice.integration.api.admin.AdministrationAPIIntegrationTester; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientConnectionDAO; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientEventDAO; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; +import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientConnectionService; +import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.AbstractLogLevelCountIndicator; + +@Sql(scripts = { "classpath:schema-test.sql", "classpath:data-test.sql", "classpath:data-test-additional.sql" }) +public class ClientEventServiceTest extends AdministrationAPIIntegrationTester { + + @Autowired + private ClientConnectionDAO clientConnectionDAO; + @Autowired + private ClientEventDAO clientEventDAO; + @Autowired + private SEBClientConnectionService sebClientConnectionService; + + @Test + public void testCreateLogEvents() { + + final ClientConnection connection = this.clientConnectionDAO + .createNew(new ClientConnection(null, 1L, 2L, ConnectionStatus.ACTIVE, "token", "userId", "", "", 1L, + null, false)) + .getOrThrow(); + + assertNotNull(connection.id); + + this.clientEventDAO + .createNew(new ClientEvent(null, connection.id, EventType.INFO_LOG, 1L, 1L, 1.0, "text")) + .getOrThrow(); + + final Collection allEvents = this.clientEventDAO.allMatching(new FilterMap()).getOrThrow(); + assertNotNull(allEvents); + assertFalse(allEvents.isEmpty()); + final ClientEvent event = allEvents.iterator().next(); + assertNotNull(event); + assertEquals("text", event.text); + } + + @Test + public void testErrorLogCountIndicator() { + + final ClientConnection connection = this.clientConnectionDAO + .createNew(new ClientConnection(null, 1L, 2L, ConnectionStatus.ACTIVE, "token1", "userId", "", "", 1L, + null, false)) + .getOrThrow(); + + assertNotNull(connection.id); + + final ClientConnectionData connectionData = + this.sebClientConnectionService.getExamSessionService().getConnectionData("token1") + .getOrThrow(); + + assertNotNull(connectionData); + final Optional findFirst = connectionData.indicatorValues + .stream() + .filter(indicator -> indicator.getType() == IndicatorType.ERROR_COUNT) + .findFirst(); + assertTrue(findFirst.isPresent()); + final IndicatorValue clientIndicator = findFirst.get(); + assertEquals("0", IndicatorValue.getDisplayValue(clientIndicator)); + + this.sebClientConnectionService.notifyClientEvent( + "token1", + new ClientEvent(null, connection.id, EventType.ERROR_LOG, 1L, 1L, 1.0, "some error")); + + assertEquals("1", IndicatorValue.getDisplayValue(clientIndicator)); + + this.sebClientConnectionService.notifyClientEvent( + "token1", + new ClientEvent(null, connection.id, EventType.ERROR_LOG, 1L, 1L, 1.0, "some error")); + + assertEquals("2", IndicatorValue.getDisplayValue(clientIndicator)); + + // test reset indicator value and load it from persistent storage + ((AbstractLogLevelCountIndicator) clientIndicator).reset(); + assertEquals("2", IndicatorValue.getDisplayValue(clientIndicator)); + + } + + @Test + public void testInfoLogWithTagCountIndicator() { + + final ClientConnection connection = this.clientConnectionDAO + .createNew(new ClientConnection(null, 1L, 2L, ConnectionStatus.ACTIVE, "token2", "userId", "", "", 1L, + null, false)) + .getOrThrow(); + + assertNotNull(connection.id); + + final ClientConnectionData connectionData = + this.sebClientConnectionService.getExamSessionService().getConnectionData("token2") + .getOrThrow(); + + assertNotNull(connectionData); + final Optional findFirst = connectionData.indicatorValues + .stream() + .filter(indicator -> indicator.getType() == IndicatorType.INFO_COUNT) + .findFirst(); + assertTrue(findFirst.isPresent()); + final IndicatorValue clientIndicator = findFirst.get(); + + assertEquals("0", IndicatorValue.getDisplayValue(clientIndicator)); + + this.sebClientConnectionService.notifyClientEvent( + "token2", + new ClientEvent(null, connection.id, EventType.INFO_LOG, 1L, 1L, 1.0, "some error")); + assertEquals("0", IndicatorValue.getDisplayValue(clientIndicator)); + this.sebClientConnectionService.notifyClientEvent( + "token2", + new ClientEvent(null, connection.id, EventType.INFO_LOG, 1L, 1L, 1.0, " some error")); + assertEquals("1", IndicatorValue.getDisplayValue(clientIndicator)); + this.sebClientConnectionService.notifyClientEvent( + "token2", + new ClientEvent(null, connection.id, EventType.INFO_LOG, 1L, 1L, 1.0, "some error")); + assertEquals("1", IndicatorValue.getDisplayValue(clientIndicator)); + this.sebClientConnectionService.notifyClientEvent( + "token2", + new ClientEvent(null, connection.id, EventType.INFO_LOG, 1L, 1L, 1.0, " some error")); + assertEquals("2", IndicatorValue.getDisplayValue(clientIndicator)); + this.sebClientConnectionService.notifyClientEvent( + "token2", + new ClientEvent(null, connection.id, EventType.INFO_LOG, 1L, 1L, 1.0, "some error")); + + assertEquals("2", IndicatorValue.getDisplayValue(clientIndicator)); + + this.sebClientConnectionService.notifyClientEvent( + "token2", + new ClientEvent(null, connection.id, EventType.INFO_LOG, 1L, 1L, 1.0, " some error")); + + // test reset indicator value and load it from persistent storage + ((AbstractLogLevelCountIndicator) clientIndicator).reset(); + assertEquals("3", IndicatorValue.getDisplayValue(clientIndicator)); + + } + +} diff --git a/src/test/resources/data-test-additional.sql b/src/test/resources/data-test-additional.sql index 8251842a..07bf4bdf 100644 --- a/src/test/resources/data-test-additional.sql +++ b/src/test/resources/data-test-additional.sql @@ -12,13 +12,18 @@ INSERT IGNORE INTO exam VALUES ; INSERT IGNORE INTO indicator VALUES - (1, 2, 'LAST_PING', 'Ping', 'dcdcdc', null, null) + (1, 2, 'LAST_PING', 'Ping', 'dcdcdc', null, null), + (2, 2, 'ERROR_COUNT', 'errors', 'dcdcdc', null, null), + (3, 2, 'INFO_COUNT', 'errors ', 'dcdcdc', null, 'vip,top') ; INSERT IGNORE INTO threshold VALUES (1, 1, 1000.0000, '22b14c', null), (2, 1, 2000.0000, 'ff7e00', null), - (3, 1, 5000.0000, 'ed1c24', null) + (3, 1, 5000.0000, 'ed1c24', null), + (4, 2, 1, '22b14c', null), + (5, 2, 2, 'ff7e00', null), + (6, 2, 5, 'ed1c24', null) ; INSERT IGNORE INTO view VALUES