diff --git a/pom.xml b/pom.xml index 2939d6a2..47304872 100644 --- a/pom.xml +++ b/pom.xml @@ -25,6 +25,7 @@ UTF-8 + @@ -45,6 +46,17 @@ maven-compiler-plugin ${java.version} + UTF-8 + + + + org.apache.maven.plugins + maven-surefire-plugin + + UTF-8 + + UTF-8 + 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 118c5ccf..0721d85f 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 @@ -12,6 +12,7 @@ import java.util.Base64; import java.util.Base64.Encoder; import java.util.Collection; import java.util.Optional; +import java.util.function.Supplier; import org.apache.commons.lang3.BooleanUtils; import org.eclipse.rap.rwt.RWT; @@ -203,25 +204,15 @@ public class MonitoringClientConnection implements TemplateComposer { getConnectionData, indicators); - this.serverPushService.runServerPush( - new ServerPushContext( - content, - Utils.truePredicate(), - MonitoringRunningExam.createServerPushUpdateErrorHandler(this.pageService, pageContext)), - this.pollInterval, - context1 -> clientConnectionDetails.updateData(), - context -> clientConnectionDetails.updateGUI()); - - final PageService.PageActionBuilder actionBuilder = this.pageService - .pageActionBuilder( - pageContext - .clearAttributes() - .clearEntityKeys()); - // NOTIFICATIONS - final boolean hasNotification = BooleanUtils.isTrue(connectionData.pendingNotification()); - if (hasNotification) { - // add notification table + final boolean hasNotifications = BooleanUtils.isTrue(connectionData.pendingNotification()); + Supplier> _notificationTableSupplier = () -> null; + if (hasNotifications) { + final PageService.PageActionBuilder actionBuilder = this.pageService + .pageActionBuilder( + pageContext + .clearAttributes() + .clearEntityKeys()); widgetFactory.addFormSubContextHeader( content, @@ -281,9 +272,28 @@ public class MonitoringClientConnection implements TemplateComposer { NOTIFICATION_LIST_NO_SELECTION_KEY) .publishIf(() -> currentUser.get().hasRole(UserRole.EXAM_SUPPORTER), false); + + _notificationTableSupplier = () -> notificationTable; } + final Supplier> notificationTableSupplier = _notificationTableSupplier; + // server push update + this.serverPushService.runServerPush( + new ServerPushContext( + content, + Utils.truePredicate(), + MonitoringRunningExam.createServerPushUpdateErrorHandler(this.pageService, pageContext)), + this.pollInterval, + context -> clientConnectionDetails.updateData(), + context -> clientConnectionDetails.updateGUI(notificationTableSupplier, pageContext)); + // CLIENT EVENTS + final PageService.PageActionBuilder actionBuilder = this.pageService + .pageActionBuilder( + pageContext + .clearAttributes() + .clearEntityKeys()); + widgetFactory.addFormSubContextHeader( content, EVENT_LIST_TITLE_KEY, diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/session/ClientConnectionDetails.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/ClientConnectionDetails.java index 5e2c4aeb..a606370c 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/session/ClientConnectionDetails.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/ClientConnectionDetails.java @@ -11,6 +11,7 @@ package ch.ethz.seb.sebserver.gui.service.session; import java.util.Collection; import java.util.Map; import java.util.function.Consumer; +import java.util.function.Supplier; import org.apache.commons.lang3.BooleanUtils; import org.eclipse.swt.graphics.Color; @@ -23,8 +24,11 @@ import ch.ethz.seb.sebserver.gbl.model.Domain; import ch.ethz.seb.sebserver.gbl.model.exam.Exam; import ch.ethz.seb.sebserver.gbl.model.exam.Indicator; import ch.ethz.seb.sebserver.gbl.model.exam.QuizData; +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.ClientNotification; import ch.ethz.seb.sebserver.gbl.model.session.IndicatorValue; +import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition; import ch.ethz.seb.sebserver.gui.form.Form; import ch.ethz.seb.sebserver.gui.form.FormBuilder; import ch.ethz.seb.sebserver.gui.form.FormHandle; @@ -32,8 +36,11 @@ import ch.ethz.seb.sebserver.gui.service.ResourceService; import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; import ch.ethz.seb.sebserver.gui.service.page.PageContext; import ch.ethz.seb.sebserver.gui.service.page.PageService; +import ch.ethz.seb.sebserver.gui.service.page.event.ActionEvent; +import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall; import ch.ethz.seb.sebserver.gui.service.session.IndicatorData.ThresholdColor; +import ch.ethz.seb.sebserver.gui.table.EntityTable; public class ClientConnectionDetails { @@ -50,6 +57,7 @@ public class ClientConnectionDetails { private static final int NUMBER_OF_NONE_INDICATOR_ROWS = 3; + private final PageService pageService; private final ResourceService resourceService; private final Map indicatorMapping; private final RestCall.RestCallBuilder restCallBuilder; @@ -69,6 +77,7 @@ public class ClientConnectionDetails { final Display display = pageContext.getRoot().getDisplay(); + this.pageService = pageService; this.resourceService = pageService.getResourceService(); this.restCallBuilder = restCallBuilder; this.colorData = new ColorData(display); @@ -132,9 +141,13 @@ public class ClientConnectionDetails { .toBoolean(connectionData.missingPing); } this.connectionData = connectionData; + } - public void updateGUI() { + public void updateGUI( + final Supplier> notificationTableSupplier, + final PageContext pageContext) { + if (this.connectionData == null) { return; } @@ -192,6 +205,29 @@ public class ClientConnectionDetails { } } }); + + // update notifications + final EntityTable notificationTable = notificationTableSupplier.get(); + if (notificationTable != null && this.connectionData.clientConnection.status == ConnectionStatus.CLOSED) { + reloadPage(pageContext); + } else { + if (BooleanUtils.isTrue(this.connectionData.pendingNotification())) { + if (notificationTable == null) { + reloadPage(pageContext); + } else { + notificationTable.refreshPageSize(); + } + } + } + } + + private void reloadPage(final PageContext pageContext) { + final PageAction pageReloadAction = this.pageService.pageActionBuilder(pageContext) + .newAction(ActionDefinition.MONITOR_EXAM_CLIENT_CONNECTION) + .create(); + this.pageService.firePageEvent( + new ActionEvent(pageReloadAction), + pageContext); } } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/table/EntityTable.java b/src/main/java/ch/ethz/seb/sebserver/gui/table/EntityTable.java index f008446d..ee3a6bba 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/table/EntityTable.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/table/EntityTable.java @@ -763,4 +763,19 @@ public class EntityTable { } } + public void refreshPageSize() { + if (this.pageSupplier.newBuilder() + .withPaging(this.pageNumber, this.pageSize) + .withSorting(this.sortColumn, this.sortOrder) + .withQueryParams((this.filter != null) ? this.filter.getFilterParameter() : null) + .withQueryParams(this.staticQueryParams) + .apply(this.pageSupplierAdapter) + .getPage() + .map(page -> page.content.size()) + .map(size -> size != this.table.getItems().length) + .getOr(false)) { + reset(); + } + } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamDAOImpl.java index d69401ce..76cbb47c 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamDAOImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamDAOImpl.java @@ -761,7 +761,7 @@ public class ExamDAOImpl implements ExamDAO { entry.getValue(), cached) .onError(error -> log.error( - "Failed to get quizzes form LMS Setup: {}", + "Failed to get quizzes from LMS Setup: {}", entry.getKey(), error)) .getOr(Collections.emptyList()) .stream()) diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/MockupLmsAPITemplate.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/MockupLmsAPITemplate.java index 13ffe912..16bd23ba 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/MockupLmsAPITemplate.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/MockupLmsAPITemplate.java @@ -65,7 +65,7 @@ final class MockupLmsAPITemplate implements LmsAPITemplate { "2020-01-01T09:00:00Z", null, "http://lms.mockup.com/api/")); this.mockups.add(new QuizData( "quiz2", institutionId, lmsSetupId, lmsType, "Demo Quiz 2 (MOCKUP)", "Demo Quiz Mockup", - "2020-01-01T09:00:00Z", "2021-01-01T09:00:00Z", "http://lms.mockup.com/api/")); + "2020-01-01T09:00:00Z", "2022-01-01T09:00:00Z", "http://lms.mockup.com/api/")); this.mockups.add(new QuizData( "quiz3", institutionId, lmsSetupId, lmsType, "Demo Quiz 3 (MOCKUP)", "Demo Quiz Mockup", "2018-07-30T09:00:00Z", "2018-08-01T00:00:00Z", "http://lms.mockup.com/api/")); @@ -74,13 +74,13 @@ final class MockupLmsAPITemplate implements LmsAPITemplate { "2018-01-01T00:00:00Z", "2019-01-01T00:00:00Z", "http://lms.mockup.com/api/")); this.mockups.add(new QuizData( "quiz5", institutionId, lmsSetupId, lmsType, "Demo Quiz 5 (MOCKUP)", "Demo Quiz Mockup", - "2018-01-01T09:00:00Z", "2021-01-01T09:00:00Z", "http://lms.mockup.com/api/")); + "2018-01-01T09:00:00Z", "2022-01-01T09:00:00Z", "http://lms.mockup.com/api/")); this.mockups.add(new QuizData( "quiz6", institutionId, lmsSetupId, lmsType, "Demo Quiz 6 (MOCKUP)", "Demo Quiz Mockup", - "2019-01-01T09:00:00Z", "2021-01-01T09:00:00Z", "http://lms.mockup.com/api/")); + "2019-01-01T09:00:00Z", "2022-01-01T09:00:00Z", "http://lms.mockup.com/api/")); this.mockups.add(new QuizData( "quiz7", institutionId, lmsSetupId, lmsType, "Demo Quiz 7 (MOCKUP)", "Demo Quiz Mockup", - "2018-01-01T09:00:00Z", "2021-01-01T09:00:00Z", "http://lms.mockup.com/api/")); + "2018-01-01T09:00:00Z", "2022-01-01T09:00:00Z", "http://lms.mockup.com/api/")); this.mockups.add(new QuizData( "quiz10", institutionId, lmsSetupId, lmsType, "Demo Quiz 10 (MOCKUP)", diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleCourseAccess.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleCourseAccess.java index d47ffb77..d2641b83 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleCourseAccess.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleCourseAccess.java @@ -217,7 +217,7 @@ public class MoodleCourseAccess extends CourseAccess { } } else if (this.moodleCourseDataLazyLoader.isLongRunningTask()) { // on long running tasks if we have a different fromCutTime as before - // kick off the lazy loadung task imeditially with the new time filter + // kick off the lazy loading task immediately with the new time filter if (fromCutTime > 0 && fromCutTime != this.moodleCourseDataLazyLoader.getFromCutTime()) { this.moodleCourseDataLazyLoader.setFromCutTime(fromCutTime); this.moodleCourseDataLazyLoader.loadAsync(restTemplate); 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 index 5bfacd70..a25bb140 100644 --- 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 @@ -10,6 +10,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.session; import java.util.List; +import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent; import ch.ethz.seb.sebserver.gbl.model.session.ClientNotification; import ch.ethz.seb.sebserver.gbl.util.Result; @@ -27,6 +28,8 @@ public interface SEBClientNotificationService { Result> getPendingNotifications(Long clientConnectionId); + void confirmPendingNotification(ClientEvent event, String connectionToken); + Result confirmPendingNotification( Long notificationId, Long examId, 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 index 38479cb4..c93bc823 100644 --- 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 @@ -12,6 +12,7 @@ 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.model.session.ClientConnection.ConnectionStatus; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientNotificationService; @@ -32,6 +33,17 @@ public class InternalClientConnectionDataFactory { } public ClientConnectionDataInternal createClientConnectionData(final ClientConnection clientConnection) { + + if (clientConnection.status == ConnectionStatus.CLOSED + || clientConnection.status == ConnectionStatus.DISABLED) { + + // dispose notification indication for closed or disabled connection + return new ClientConnectionDataInternal( + clientConnection, + () -> false, + this.clientIndicatorFactory.createFor(clientConnection)); + } + return new ClientConnectionDataInternal( clientConnection, () -> this.sebClientNotificationService 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 72e717e3..3c525551 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,7 +28,6 @@ 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; @@ -40,8 +39,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.SEBClientNotificationService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientInstructionService; +import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientNotificationService; import ch.ethz.seb.sebserver.webservice.weblayer.api.APIConstraintViolationException; @Lazy @@ -524,14 +523,22 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic event, activeClientConnection.getConnectionId())); - 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)); + switch (event.eventType) { + case NOTIFICATION: { + this.sebClientNotificationService.notifyNewNotification(activeClientConnection.getConnectionId()); + break; + } + case NOTIFICATION_CONFIRMED: { + this.sebClientNotificationService.confirmPendingNotification(event, connectionToken); + break; + } + default: { + // 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/SEBClientNotificationServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientNotificationServiceImpl.java index 6f612223..19254f92 100644 --- 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 @@ -15,9 +15,12 @@ import java.util.List; import java.util.Map; import java.util.Set; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; 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.model.session.ClientInstruction; import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction.InstructionType; import ch.ethz.seb.sebserver.gbl.model.session.ClientNotification; @@ -32,6 +35,8 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientNotificati @WebServiceProfile public class SEBClientNotificationServiceImpl implements SEBClientNotificationService { + private static final Logger log = LoggerFactory.getLogger(SEBClientNotificationServiceImpl.class); + private static final String CONFIRM_INSTRUCTION_ATTR_ID = "id"; private static final String CONFIRM_INSTRUCTION_ATTR_TYPE = "type"; @@ -69,6 +74,24 @@ public class SEBClientNotificationServiceImpl implements SEBClientNotificationSe return this.clientEventDAO.getPendingNotifications(clientConnectionId); } + @Override + public void confirmPendingNotification(final ClientEvent event, final String connectionToken) { + try { + final Long notificationId = (long) event.getValue(); + + this.clientEventDAO.getPendingNotification(notificationId) + .flatMap(notification -> this.clientEventDAO.confirmPendingNotification( + notificationId, + notification.connectionId)) + .map(this::removeFromCache); + + } catch (final Exception e) { + log.error( + "Failed to confirm pending notification from SEB Client side. Connection token: {} confirm event: {}", + connectionToken, event, e); + } + } + @Override public Result confirmPendingNotification( final Long notificationId, diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index 2497d733..536b6e35 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -1554,13 +1554,13 @@ sebserver.monitoring.exam.connection.eventlist.text=Text sebserver.monitoring.exam.connection.eventlist.text.tooltip=The text of the log event

{0} sebserver.monitoring.exam.connection.event.type.UNKNOWN=Unknown -sebserver.monitoring.exam.connection.event.type.DEBUG_LOG=Debug -sebserver.monitoring.exam.connection.event.type.INFO_LOG=Info -sebserver.monitoring.exam.connection.event.type.WARN_LOG=Warn -sebserver.monitoring.exam.connection.event.type.ERROR_LOG=Error +sebserver.monitoring.exam.connection.event.type.DEBUG_LOG=Debug Log +sebserver.monitoring.exam.connection.event.type.INFO_LOG=Info Log +sebserver.monitoring.exam.connection.event.type.WARN_LOG=Warn Log +sebserver.monitoring.exam.connection.event.type.ERROR_LOG=Error Log sebserver.monitoring.exam.connection.event.type.LAST_PING=Last Ping sebserver.monitoring.exam.connection.event.type.NOTIFICATION=Notification (pending) -sebserver.monitoring.exam.connection.event.type.NOTIFICATION_CONFIRM=Notification (confirmed) +sebserver.monitoring.exam.connection.event.type.NOTIFICATION_CONFIRMED=Notification (confirmed) sebserver.monitoring.exam.connection.notification.type.UNKNOWN=Unknown sebserver.monitoring.exam.connection.notification.type.LOCK_SCREEN=Lock Screen