SEBSERV-147 GUI implementation

This commit is contained in:
anhefti 2020-12-01 11:46:41 +01:00
parent 8e10995f0d
commit 3052ec1f59
24 changed files with 329 additions and 69 deletions

View file

@ -100,7 +100,6 @@ public final class Indicator implements Entity {
public final String defaultIcon;
@JsonProperty(INDICATOR.ATTR_TAGS)
@Size(min = 3, max = 255, message = "indicator:tag:size:{min}:{max}:${validatedValue}")
public final String tags;
@JsonProperty(THRESHOLD.REFERENCE_NAME)

View file

@ -33,14 +33,17 @@ public class ClientConnectionData {
public final List<? extends IndicatorValue> indicatorValues;
public final Boolean missingPing;
public final Boolean pendingNotification;
@JsonCreator
public ClientConnectionData(
@JsonProperty(ATTR_MISSING_PING) final Boolean missingPing,
@JsonProperty(ATTR_PENDING_NOTIFICATION) final Boolean pendingNotification,
@JsonProperty(ATTR_CLIENT_CONNECTION) final ClientConnection clientConnection,
@JsonProperty(ATTR_INDICATOR_VALUE) final Collection<? extends SimpleIndicatorValue> indicatorValues) {
this.missingPing = missingPing;
this.pendingNotification = pendingNotification;
this.clientConnection = clientConnection;
this.indicatorValues = Utils.immutableListOf(indicatorValues);
}
@ -50,6 +53,7 @@ public class ClientConnectionData {
final List<? extends IndicatorValue> indicatorValues) {
this.missingPing = null;
this.pendingNotification = Boolean.FALSE;
this.clientConnection = clientConnection;
this.indicatorValues = Utils.immutableListOf(indicatorValues);
}
@ -61,7 +65,7 @@ public class ClientConnectionData {
@JsonProperty(ATTR_PENDING_NOTIFICATION)
public Boolean pendingNotification() {
return false;
return this.pendingNotification;
}
@JsonIgnore

View file

@ -26,7 +26,8 @@ public final class ClientInstruction {
public enum InstructionType {
SEB_QUIT,
SEB_PROCTORING,
SEB_RECONFIGURE_SETTINGS
SEB_RECONFIGURE_SETTINGS,
NOTIFICATION_CONFIRM
}
public enum ProctoringInstructionMethod {

View file

@ -0,0 +1,109 @@
/*
* 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.gbl.model.session;
import java.util.Arrays;
import org.apache.commons.lang3.StringUtils;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.model.Domain;
public class ClientNotification extends ClientEvent {
public static enum NotificationType {
UNKNOWN(null),
LOCK_SCREEN("lockscreen"),
RAISE_HAND("raisehand");
public final String typeName;
private NotificationType(final String typeName) {
this.typeName = typeName;
}
public static NotificationType getNotificationType(final String text) {
if (StringUtils.isBlank(text)) {
return NotificationType.UNKNOWN;
}
return Arrays.asList(NotificationType.values())
.stream()
.filter(type -> type.typeName != null &&
text.startsWith(Constants.ANGLE_BRACE_OPEN + type.typeName + Constants.ANGLE_BRACE_CLOSE))
.findFirst()
.orElse(NotificationType.UNKNOWN);
}
}
public static final String ATTR_NOTIFICATION_TYPE = "notificationType";
@JsonProperty(ATTR_NOTIFICATION_TYPE)
public final NotificationType notificationType;
public ClientNotification(
final Long id,
final Long connectionId,
final EventType eventType,
final Long clientTime,
final Long serverTime,
final Double numValue,
final String text) {
super(id, connectionId, eventType, clientTime, serverTime, numValue, text);
this.notificationType = NotificationType.getNotificationType(text);
}
@JsonCreator
public ClientNotification(
@JsonProperty(Domain.CLIENT_EVENT.ATTR_ID) final Long id,
@JsonProperty(Domain.CLIENT_EVENT.ATTR_CLIENT_CONNECTION_ID) final Long connectionId,
@JsonProperty(Domain.CLIENT_EVENT.ATTR_TYPE) final EventType eventType,
@JsonProperty(ATTR_TIMESTAMP) final Long clientTime,
@JsonProperty(Domain.CLIENT_EVENT.ATTR_SERVER_TIME) final Long serverTime,
@JsonProperty(Domain.CLIENT_EVENT.ATTR_NUMERIC_VALUE) final Double numValue,
@JsonProperty(Domain.CLIENT_EVENT.ATTR_TEXT) final String text,
@JsonProperty(ATTR_NOTIFICATION_TYPE) final NotificationType notificationType) {
super(id, connectionId, eventType, clientTime, serverTime, numValue, text);
this.notificationType = notificationType;
}
public NotificationType getNotificationType() {
return this.notificationType;
}
@Override
public String toString() {
final StringBuilder builder = new StringBuilder();
builder.append("ClientNotification [notificationType=");
builder.append(this.notificationType);
builder.append(", id=");
builder.append(this.id);
builder.append(", connectionId=");
builder.append(this.connectionId);
builder.append(", eventType=");
builder.append(this.eventType);
builder.append(", clientTime=");
builder.append(this.clientTime);
builder.append(", serverTime=");
builder.append(this.serverTime);
builder.append(", numValue=");
builder.append(this.numValue);
builder.append(", text=");
builder.append(this.text);
builder.append("]");
return builder.toString();
}
}

View file

@ -35,6 +35,7 @@ import ch.ethz.seb.sebserver.gbl.model.exam.SEBProctoringConnectionData;
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.ClientNotification;
import ch.ethz.seb.sebserver.gbl.model.session.ExtendedClientEvent;
import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom;
import ch.ethz.seb.sebserver.gbl.model.user.UserRole;
@ -88,7 +89,11 @@ public class MonitoringClientConnection implements TemplateComposer {
private static final LocTextKey NOTIFICATION_LIST_TITLE_TOOLTIP_KEY =
new LocTextKey("sebserver.monitoring.exam.connection.notificationlist.title.tooltip");
private static final LocTextKey NOTIFICATION_LIST_CONFIRM_TEXT_KEY =
new LocTextKey("monitoring.exam.connection.action.confirm.notification.text");
new LocTextKey("sebserver.monitoring.exam.connection.action.confirm.notification.text");
private static final LocTextKey NOTIFICATION_LIST_NO_SELECTION_KEY =
new LocTextKey("sebserver.monitoring.exam.connection.notificationlist.pleaseSelect");
private static final LocTextKey NOTIFICATION_LIST_COLUMN_TYPE_KEY =
new LocTextKey("sebserver.monitoring.exam.connection.notificationlist.type");
private static final LocTextKey EVENT_LIST_TITLE_KEY =
new LocTextKey("sebserver.monitoring.exam.connection.eventlist.title");
@ -98,6 +103,7 @@ public class MonitoringClientConnection implements TemplateComposer {
new LocTextKey("sebserver.monitoring.exam.connection.eventlist.empty");
private static final LocTextKey LIST_COLUMN_TYPE_KEY =
new LocTextKey("sebserver.monitoring.exam.connection.eventlist.type");
private static final LocTextKey LIST_COLUMN_CLIENT_TIME_KEY =
new LocTextKey("sebserver.monitoring.exam.connection.eventlist.clienttime");
private static final LocTextKey LIST_COLUMN_SERVER_TIME_KEY =
@ -212,6 +218,7 @@ public class MonitoringClientConnection implements TemplateComposer {
.clearAttributes()
.clearEntityKeys());
// NOTIFICATIONS
final boolean hasNotification = BooleanUtils.isTrue(connectionData.pendingNotification());
if (hasNotification) {
// add notification table
@ -221,7 +228,7 @@ public class MonitoringClientConnection implements TemplateComposer {
NOTIFICATION_LIST_TITLE_KEY,
NOTIFICATION_LIST_TITLE_TOOLTIP_KEY);
final EntityTable<ClientEvent> notificationTable = this.pageService.remoteListTableBuilder(
final EntityTable<ClientNotification> notificationTable = this.pageService.remoteListTableBuilder(
restService.getRestCall(GetPendingClientNotifications.class),
EntityType.CLIENT_EVENT)
.withRestCallAdapter(builder -> builder.withURIVariable(
@ -230,22 +237,22 @@ public class MonitoringClientConnection implements TemplateComposer {
.withURIVariable(
API.EXAM_API_SEB_CONNECTION_TOKEN,
connectionToken))
.withPaging(5)
.withColumn(new ColumnDefinition<ClientEvent>(
Domain.CLIENT_EVENT.ATTR_TYPE,
LIST_COLUMN_TYPE_KEY,
this.resourceService::getEventTypeName)
.withPaging(-1)
.withColumn(new ColumnDefinition<ClientNotification>(
ClientNotification.ATTR_NOTIFICATION_TYPE,
NOTIFICATION_LIST_COLUMN_TYPE_KEY,
this.resourceService::getNotificationTypeName)
.sortable()
.widthProportion(2))
.withColumn(new ColumnDefinition<>(
.withColumn(new ColumnDefinition<ClientNotification>(
Domain.CLIENT_EVENT.ATTR_TEXT,
LIST_COLUMN_TEXT_KEY,
ClientEvent::getText)
.sortable()
.withCellTooltip()
.widthProportion(4))
.withColumn(new ColumnDefinition<>(
.withColumn(new ColumnDefinition<ClientNotification>(
Domain.CLIENT_EVENT.ATTR_SERVER_TIME,
new LocTextKey(LIST_COLUMN_SERVER_TIME_KEY.name,
this.i18nSupport.getUsersTimeZoneTitleSuffix()),
@ -268,10 +275,15 @@ public class MonitoringClientConnection implements TemplateComposer {
.newAction(ActionDefinition.MONITOR_EXAM_CLIENT_CONNECTION_CONFIRM_NOTIFICATION)
.withParentEntityKey(parentEntityKey)
.withConfirm(() -> NOTIFICATION_LIST_CONFIRM_TEXT_KEY)
.withExec(action -> this.confirmNotification(action, connectionData))
.publishIf(() -> currentUser.get().hasRole(UserRole.EXAM_SUPPORTER));
.withSelect(
() -> notificationTable.getSelection(),
action -> this.confirmNotification(action, connectionData),
NOTIFICATION_LIST_NO_SELECTION_KEY)
.publishIf(() -> currentUser.get().hasRole(UserRole.EXAM_SUPPORTER), false);
}
// CLIENT EVENTS
widgetFactory.addFormSubContextHeader(
content,
EVENT_LIST_TITLE_KEY,
@ -401,7 +413,13 @@ public class MonitoringClientConnection implements TemplateComposer {
.call()
.getOrThrow();
return pageAction;
return pageAction
.withEntityKey(
new EntityKey(connectionData.getConnectionId(),
EntityType.CLIENT_CONNECTION))
.withAttribute(
Domain.CLIENT_CONNECTION.ATTR_CONNECTION_TOKEN,
connectionData.clientConnection.connectionToken);
}
private PageAction openExamCollectionProctorScreen(

View file

@ -25,6 +25,8 @@ public enum ActionCategory {
SEB_CONFIG_TEMPLATE_LIST(new LocTextKey("sebserver.configtemplate.list.actions"), 1),
SEB_CONFIG_TEMPLATE_ATTRIBUTE_LIST(new LocTextKey("sebserver.configtemplate.attr.list.actions"), 1),
RUNNING_EXAM_LIST(new LocTextKey("sebserver.monitoring.exam.list.actions"), 1),
EXAM_MONITORING_NOTIFICATION_LIST(new LocTextKey("sebserver.monitoring.exam.connection.notificationlist.actions"),
1),
CLIENT_EVENT_LIST(new LocTextKey("sebserver.monitoring.exam.connection.list.actions"), 1),
LOGS_USER_ACTIVITY_LIST(new LocTextKey("sebserver.userlogs.list.actions"), 1),
LOGS_SEB_CLIENT_LIST(new LocTextKey("sebserver.userlogs.list.actions"), 1),

View file

@ -644,7 +644,7 @@ public enum ActionDefinition {
new LocTextKey("sebserver.monitoring.exam.connection.action.confirm.notification"),
ImageIcon.YES,
PageStateDefinitionImpl.MONITORING_CLIENT_CONNECTION,
ActionCategory.FORM),
ActionCategory.EXAM_MONITORING_NOTIFICATION_LIST),
MONITOR_EXAM_QUIT_SELECTED(
new LocTextKey("sebserver.monitoring.exam.connection.action.instruction.quit.selected"),

View file

@ -52,6 +52,8 @@ 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.ClientNotification;
import ch.ethz.seb.sebserver.gbl.model.session.ClientNotification.NotificationType;
import ch.ethz.seb.sebserver.gbl.model.user.UserActivityLog;
import ch.ethz.seb.sebserver.gbl.model.user.UserInfo;
import ch.ethz.seb.sebserver.gbl.model.user.UserLogActivityType;
@ -118,6 +120,8 @@ public class ResourceService {
AttributeType.INLINE_TABLE);
public static final String CLIENT_EVENT_TYPE_PREFIX = "sebserver.monitoring.exam.connection.event.type.";
public static final String CLIENT_NOTIFICATION_TYPE_PREFIX =
"sebserver.monitoring.exam.connection.notification.type.";
public static final String USER_ACTIVITY_TYPE_PREFIX = "sebserver.overall.types.activityType.";
public static final String ENTITY_TYPE_PREFIX = "sebserver.overall.types.entityType.";
public static final String SEB_CONNECTION_STATUS_KEY_PREFIX = "sebserver.monitoring.exam.connection.status.";
@ -200,6 +204,22 @@ public class ResourceService {
return this.i18nSupport.getText(CLIENT_EVENT_TYPE_PREFIX + eventType.name(), eventType.name());
}
public String getNotificationTypeName(final ClientNotification notification) {
if (notification == null) {
return getEventTypeName(EventType.UNKNOWN);
}
return getNotificationTypeName(notification.getNotificationType());
}
public String getNotificationTypeName(final NotificationType notificationType) {
if (notificationType == null) {
return Constants.EMPTY_NOTE;
}
return this.i18nSupport.getText(
CLIENT_NOTIFICATION_TYPE_PREFIX + notificationType.name(),
notificationType.name());
}
public List<Tuple<String>> indicatorTypeResources() {
return Arrays.stream(IndicatorType.values())
.map(type -> new Tuple3<>(

View file

@ -339,6 +339,7 @@ public class PageContextImpl implements PageContext {
this.i18nSupport);
messageBox.setMarkupEnabled(true);
messageBox.open(null);
log.error("Unexpected error on GUI: ", error);
}
@Override

View file

@ -32,7 +32,7 @@ public class ConfirmPendingClientNotification extends RestCall<Void> {
new TypeReference<Void>() {
}),
HttpMethod.POST,
MediaType.APPLICATION_JSON_UTF8,
MediaType.APPLICATION_FORM_URLENCODED,
API.EXAM_MONITORING_ENDPOINT
+ API.PARENT_MODEL_ID_VAR_PATH_SEGMENT
+ API.EXAM_MONITORING_NOTIFICATION_ENDPOINT

View file

@ -19,20 +19,20 @@ import com.fasterxml.jackson.core.type.TypeReference;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
import ch.ethz.seb.sebserver.gbl.model.session.ClientNotification;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
@Lazy
@Component
@GuiProfile
public class GetPendingClientNotifications extends RestCall<Collection<ClientEvent>> {
public class GetPendingClientNotifications extends RestCall<Collection<ClientNotification>> {
public GetPendingClientNotifications() {
super(new TypeKey<>(
CallType.GET_LIST,
EntityType.CLIENT_EVENT,
new TypeReference<Collection<ClientEvent>>() {
new TypeReference<Collection<ClientNotification>>() {
}),
HttpMethod.GET,
MediaType.APPLICATION_FORM_URLENCODED,

View file

@ -513,13 +513,9 @@ public final class ClientConnectionTable {
if (list != null && list.size() > 1) {
tableItem.setBackground(0, ClientConnectionTable.this.colorData.color3);
tableItem.setForeground(0, ClientConnectionTable.this.lightFontColor);
tableItem.setImage(1,
WidgetFactory.ImageIcon.ADD.getImage(ClientConnectionTable.this.table.getDisplay()));
} else {
tableItem.setBackground(0, null);
tableItem.setForeground(0, ClientConnectionTable.this.darkFontColor);
tableItem.setImage(0, null);
}
}
}

View file

@ -218,7 +218,7 @@ public class EntityTable<ROW> {
this.table.addListener(SWT.Selection, event -> this.notifySelectionChange());
this.navigator = new TableNavigator(this);
this.navigator = (pageSize > 0) ? new TableNavigator(this) : new TableNavigator();
createTableColumns();
this.pageNumber = initCurrentPageFromUserAttr();

View file

@ -108,6 +108,10 @@ public class RemoteListPageSupplier<T> implements PageSupplier<T> {
}
final int numOfPages = list.size() / this.pageSize;
if (numOfPages <= 0) {
return new Page<>(1, 1, this.column, list);
}
final List<T> subList = list.subList(this.pageNumber * this.pageSize,
this.pageNumber * this.pageSize + this.pageSize);
return new Page<>(numOfPages, this.pageNumber, this.column, subList);

View file

@ -102,6 +102,10 @@ public class StaticListPageSupplier<T> implements PageSupplier<T> {
}
final int numOfPages = this.list.size() / this.pageSize;
if (numOfPages <= 0) {
return new Page<>(1, 1, this.column, this.list);
}
final List<T> subList = this.list.subList(this.pageNumber * this.pageSize,
this.pageNumber * this.pageSize + this.pageSize);
return new Page<>(numOfPages, this.pageNumber, this.column, subList);

View file

@ -26,6 +26,11 @@ public class TableNavigator {
private final Composite composite;
private final EntityTable<?> entityTable;
TableNavigator() {
this.composite = null;
this.entityTable = null;
}
TableNavigator(final EntityTable<?> entityTable) {
this.composite = new Composite(entityTable.composite, SWT.NONE);
final GridData gridData = new GridData(SWT.LEFT, SWT.CENTER, true, true);
@ -37,6 +42,10 @@ public class TableNavigator {
}
public Page<?> update(final Page<?> pageData) {
if (this.composite == null) {
return pageData;
}
// clear all
PageService.clearComposite(this.composite);

View file

@ -13,6 +13,7 @@ import java.util.List;
import java.util.function.Predicate;
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;
@ -27,8 +28,10 @@ public interface ClientEventDAO extends EntityDAO<ClientEvent, ClientEvent> {
FilterMap filterMap,
Predicate<ExtendedClientEvent> predicate);
Result<List<ClientEvent>> getPendingNotifications(Long clientConnectionId);
Result<ClientNotification> getPendingNotification(Long notificationId);
Result<ClientEvent> confirmPendingNotification(Long notificationId, Long clientConnectionId);
Result<List<ClientNotification>> getPendingNotifications(Long clientConnectionId);
Result<ClientNotification> confirmPendingNotification(Long notificationId, Long clientConnectionId);
}

View file

@ -29,6 +29,7 @@ import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
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.ClientNotification;
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;
@ -187,21 +188,31 @@ public class ClientEventDAOImpl implements ClientEventDAO {
@Override
@Transactional(readOnly = true)
public Result<List<ClientEvent>> getPendingNotifications(final Long clientConnectionId) {
return Result.tryCatch(() -> this.clientEventRecordMapper.selectByExample()
public Result<ClientNotification> getPendingNotification(final Long notificationId) {
return Result.tryCatch(() -> this.clientEventRecordMapper
.selectByPrimaryKey(notificationId))
.flatMap(ClientEventDAOImpl::toClientNotificationModel);
}
@Override
@Transactional(readOnly = true)
public Result<List<ClientNotification>> 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)
.map(ClientEventDAOImpl::toClientNotificationModel)
.flatMap(DAOLoggingSupport::logAndSkipOnError)
.collect(Collectors.toList()));
}
@Override
@Transactional
public Result<ClientEvent> confirmPendingNotification(final Long notificationId, final Long clientConnectionId) {
public Result<ClientNotification> confirmPendingNotification(final Long notificationId,
final Long clientConnectionId) {
return Result.tryCatch(() -> {
final Long pk = this.clientEventRecordMapper.selectIdsByExample()
.where(ClientEventRecordDynamicSqlSupport.id, isEqualTo(notificationId))
@ -218,7 +229,7 @@ public class ClientEventDAOImpl implements ClientEventDAO {
return this.clientEventRecordMapper.selectByPrimaryKey(pk);
})
.flatMap(ClientEventDAOImpl::toDomainModel)
.flatMap(ClientEventDAOImpl::toClientNotificationModel)
.onError(TransactionHandler::rollback);
}
@ -286,6 +297,22 @@ public class ClientEventDAOImpl implements ClientEventDAO {
});
}
private static Result<ClientNotification> toClientNotificationModel(final ClientEventRecord record) {
return Result.tryCatch(() -> {
final Integer type = record.getType();
final BigDecimal numericValue = record.getNumericValue();
return new ClientNotification(
record.getId(),
record.getClientConnectionId(),
(type != null) ? EventType.byId(type) : EventType.UNKNOWN,
record.getClientTime(),
record.getServerTime(),
(numericValue != null) ? numericValue.doubleValue() : null,
record.getText());
});
}
private static Result<ClientEvent> toDomainModel(final ClientEventRecord record) {
return Result.tryCatch(() -> {

View file

@ -36,8 +36,8 @@ public interface SEBClientInstructionService {
*
* @param clientInstruction the ClientInstruction instance to register
* @return A Result refer to a void marker or to an error if happened */
default Result<Void> registerInstruction(final ClientInstruction clientInstructionn) {
return registerInstruction(clientInstructionn, false);
default Result<Void> registerInstruction(final ClientInstruction clientInstruction) {
return registerInstruction(clientInstruction, false);
}
/** Used to register a SEB client instruction for one or more active client connections

View file

@ -10,39 +10,28 @@ 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.model.session.ClientNotification;
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";
public static final String CACHE_CLIENT_NOTIFICATION = "CLIENT_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<List<ClientEvent>> getPendingNotifications(Long clientConnectionId);
Result<List<ClientNotification>> getPendingNotifications(Long clientConnectionId);
@CacheEvict(
cacheNames = CACHE_CLIENT_NOTIFICATION,
key = "#clientConnectionId")
Result<ClientEvent> confirmPendingNotification(Long notificationId, final Long clientConnectionId);
Result<ClientNotification> confirmPendingNotification(
Long notificationId,
Long examId,
String connectionToken);
@CacheEvict(
cacheNames = CACHE_CLIENT_NOTIFICATION,
key = "#clientConnectionId")
default void notifyNewNotification(final Long clientConnectionId) {
}
void notifyNewNotification(final Long clientConnectionId);
}

View file

@ -9,15 +9,22 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
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;
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.SEBClientInstructionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientNotificationService;
@Lazy
@ -25,27 +32,85 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientNotificati
@WebServiceProfile
public class SEBClientNotificationServiceImpl implements SEBClientNotificationService {
private final ClientEventDAO clientEventDAO;
private static final String CONFIRM_INSTRUCTION_ATTR_ID = "id";
private static final String CONFIRM_INSTRUCTION_ATTR_TYPE = "type";
private final ClientEventDAO clientEventDAO;
private final SEBClientInstructionService sebClientInstructionService;
private final Set<Long> pendingNotifications;
public SEBClientNotificationServiceImpl(
final ClientEventDAO clientEventDAO,
final SEBClientInstructionService sebClientInstructionService) {
public SEBClientNotificationServiceImpl(final ClientEventDAO clientEventDAO) {
this.clientEventDAO = clientEventDAO;
this.sebClientInstructionService = sebClientInstructionService;
this.pendingNotifications = new HashSet<>();
}
@Override
public Boolean hasAnyPendingNotification(final Long clientConnectionId) {
return !getPendingNotifications(clientConnectionId)
if (this.pendingNotifications.contains(clientConnectionId)) {
return true;
}
final boolean hasAnyPendingNotification = !getPendingNotifications(clientConnectionId)
.getOr(Collections.emptyList())
.isEmpty();
if (hasAnyPendingNotification) {
this.pendingNotifications.add(clientConnectionId);
}
return hasAnyPendingNotification;
}
@Override
public Result<List<ClientEvent>> getPendingNotifications(final Long clientConnectionId) {
public Result<List<ClientNotification>> getPendingNotifications(final Long clientConnectionId) {
return this.clientEventDAO.getPendingNotifications(clientConnectionId);
}
@Override
public Result<ClientEvent> confirmPendingNotification(final Long notificatioId, final Long clientConnectionId) {
return this.clientEventDAO.confirmPendingNotification(notificatioId, clientConnectionId);
public Result<ClientNotification> confirmPendingNotification(
final Long notificationId,
final Long examId,
final String connectionToken) {
return this.clientEventDAO.getPendingNotification(notificationId)
.map(notification -> this.confirmClientSide(notification, examId, connectionToken))
.flatMap(notification -> this.clientEventDAO.confirmPendingNotification(
notificationId,
notification.connectionId))
.map(this::removeFromCache);
}
@Override
public void notifyNewNotification(final Long clientConnectionId) {
this.pendingNotifications.add(clientConnectionId);
}
private ClientNotification confirmClientSide(
final ClientNotification notification,
final Long examId,
final String connectionToken) {
// create and send confirming SEB instruction to confirm the notification on client side
final Map<String, String> attributes = new HashMap<>();
attributes.put(CONFIRM_INSTRUCTION_ATTR_TYPE, notification.getNotificationType().typeName);
attributes.put(CONFIRM_INSTRUCTION_ATTR_ID, String.valueOf((int) notification.getValue()));
final ClientInstruction clientInstruction = new ClientInstruction(
null,
examId,
InstructionType.NOTIFICATION_CONFIRM,
connectionToken,
attributes);
this.sebClientInstructionService.registerInstruction(clientInstruction);
return notification;
}
private ClientNotification removeFromCache(final ClientNotification notification) {
this.pendingNotifications.remove(notification.connectionId);
return notification;
}
}

View file

@ -41,8 +41,8 @@ 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.session.ClientNotification;
import ch.ethz.seb.sebserver.gbl.model.user.UserRole;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.webservice.servicelayer.PaginationService;
@ -252,8 +252,9 @@ public class ExamMonitoringController {
API.EXAM_MONITORING_NOTIFICATION_ENDPOINT +
API.EXAM_MONITORING_SEB_CONNECTION_TOKEN_PATH_SEGMENT,
method = RequestMethod.GET,
consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
public Collection<ClientEvent> pendingNotifications(
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public Collection<ClientNotification> pendingNotifications(
@RequestParam(
name = API.PARAM_INSTITUTION_ID,
required = true,
@ -276,7 +277,7 @@ public class ExamMonitoringController {
API.MODEL_ID_VAR_PATH_SEGMENT +
API.EXAM_MONITORING_SEB_CONNECTION_TOKEN_PATH_SEGMENT,
method = RequestMethod.POST,
consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
public void confirmNotification(
@RequestParam(
name = API.PARAM_INSTITUTION_ID,
@ -286,13 +287,10 @@ public class ExamMonitoringController {
@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())
examId,
connectionToken)
.getOrThrow();
}

View file

@ -1522,9 +1522,12 @@ sebserver.monitoring.exam.connection.action.hide.undefined=Hide Undefined
sebserver.monitoring.exam.connection.action.show.undefined=Show Undefined
sebserver.monitoring.exam.connection.action.proctoring=Single Room Proctoring
sebserver.monitoring.exam.connection.action.proctoring.examroom=Exam Room Proctoring
sebserver.monitoring.exam.connection.notificationlist.actions=
sebserver.monitoring.exam.connection.action.confirm.notification=Confirm Notification
sebserver.monitoring.exam.connection.action.confirm.notification.text=Are you sure you want to confirm this pending notification?<br/><br/>Note that this will send a notification confirmation instruction to the SEB client and remove this notification from the pending list.
sebserver.monitoring.exam.connection.notificationlist.pleaseSelect=At first please select a notification form the Pending Notification list
sebserver.monitoring.exam.connection.notificationlist.title=Pending Notification
sebserver.monitoring.exam.connection.notificationlist.title.tooltip=All pending notifications sent by the SEB Client
@ -1549,6 +1552,13 @@ 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.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.notification.type.UNKNOWN=Unknown
sebserver.monitoring.exam.connection.notification.type.LOCK_SCREEN=Lock Screen
sebserver.monitoring.exam.connection.notification.type.RAISE_HAND=Raise Hand
sebserver.monitoring.exam.connection.notificationlist.type=Notification Type
sebserver.monitoring.exam.connection.status.UNDEFINED=Undefined
sebserver.monitoring.exam.connection.status.CONNECTION_REQUESTED=Connection Requested

View file

@ -244,6 +244,7 @@ public class ModelObjectJSONGenerator {
System.out.println(writerWithDefaultPrettyPrinter.writeValueAsString(domainObject));
domainObject = new ClientConnectionData(
false,
false,
new ClientConnection(
1L, 1L, 1L, ConnectionStatus.ACTIVE, UUID.randomUUID().toString(),