From 057275ba490703f9086847274b413cd8a6d0f3e0 Mon Sep 17 00:00:00 2001 From: anhefti Date: Wed, 28 Oct 2020 15:20:58 +0100 Subject: [PATCH] SEBSERV-139 added collecting all room feature --- .../ch/ethz/seb/sebserver/gbl/api/API.java | 19 +- .../content/MonitoringClientConnection.java | 31 +- .../gui/content/MonitoringRunningExam.java | 47 +- .../gui/content/action/ActionDefinition.java | 5 + .../api/exam/GetProctoringSettings.java | 2 +- .../api/exam/SaveProctoringSettings.java | 2 +- ...=> CreateCollectingAllProctoringRoom.java} | 14 +- .../DisposeCollectingAllProctoringRoom.java | 41 ++ .../api/session/GetProcotringRooms.java | 5 +- .../session/GetProctorRoomConnectionData.java | 5 +- .../session/GetProctorRoomConnections.java | 5 +- .../session/SendJoinRemoteProctoringRoom.java | 5 +- ...SendProctoringBroadcastOffInstruction.java | 5 +- .../SendProctoringBroadcastOnInstruction.java | 5 +- .../session/SendRejoinExamCollectionRoom.java | 5 +- .../service/session/ProctoringGUIService.java | 117 ++-- .../session/ExamProctoringService.java | 9 +- .../impl/ExamJITSIProctoringService.java | 30 +- .../impl/ExamProcotringRoomServiceImpl.java | 16 +- .../session/impl/ExamSessionServiceImpl.java | 11 +- .../api/ExamAdministrationController.java | 4 +- .../api/ExamMonitoringController.java | 452 +------------- .../api/ExamProctoringController.java | 587 ++++++++++++++++++ src/main/resources/messages.properties | 2 + 24 files changed, 863 insertions(+), 561 deletions(-) rename src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/{LeaveRemoteProctoringRoom.java => CreateCollectingAllProctoringRoom.java} (73%) create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/DisposeCollectingAllProctoringRoom.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamProctoringController.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 b63137fb..760df1b8 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 @@ -128,14 +128,7 @@ public final class API { public static final String EXAM_ADMINISTRATION_CHECK_RESTRICTION_PATH_SEGMENT = "/check-seb-restriction"; public static final String EXAM_ADMINISTRATION_CHECK_IMPORTED_PATH_SEGMENT = "/check-imported"; public static final String EXAM_ADMINISTRATION_SEB_RESTRICTION_CHAPTERS_PATH_SEGMENT = "/chapters"; - public static final String PROCTORING_PATH_SEGMENT = "/proctoring"; - public static final String PROCTORING_ROOMS_SEGMENT = "/rooms"; - public static final String PROCTORING_JOIN_ROOM_PATH_SEGMENT = "/join"; - public static final String PROCTORING_LEAVE_ROOM_PATH_SEGMENT = "/leave"; - public static final String PROCTORING_REJOIN_EXAM_ROOM_PATH_SEGMENT = "/rejoin-exam-room"; - public static final String PROCTORING_BROADCAST_ON_PATH_SEGMENT = "/broadcast-on"; - public static final String PROCTORING_BROADCAST_OFF_PATH_SEGMENT = "/broadcast-off"; - public static final String PROCTORING_ROOM_CONNECTIONS_PATH_SEGMENT = "/room-connections"; + public static final String EXAM_ADMINISTRATION_PROCTORING_PATH_SEGMENT = "/proctoring"; public static final String EXAM_INDICATOR_ENDPOINT = "/indicator"; @@ -177,6 +170,16 @@ public final class API { public static final String EXAM_MONITORING_SEB_CONNECTION_TOKEN_PATH_SEGMENT = "/{" + EXAM_API_SEB_CONNECTION_TOKEN + "}"; + public static final String EXAM_PROCTORING_ENDPOINT = EXAM_MONITORING_ENDPOINT + "/proctoring"; + public static final String EXAM_PROCTORING_ROOMS_SEGMENT = "/rooms"; + public static final String EXAM_PROCTORING_JOIN_ROOM_PATH_SEGMENT = "/join"; + public static final String EXAM_PROCTORING_REJOIN_COLLECTING_ROOM_PATH_SEGMENT = "/rejoin-collecting-room"; + public static final String EXAM_PROCTORING_BROADCAST_ON_PATH_SEGMENT = "/broadcast-on"; + public static final String EXAM_PROCTORING_BROADCAST_OFF_PATH_SEGMENT = "/broadcast-off"; + public static final String EXAM_PROCTORING_ROOM_CONNECTIONS_PATH_SEGMENT = "/room-connections"; + public static final String EXAM_PROCTORING_JON_ALL_COLLECTING_ROOM = "join-all-collecting-room"; + public static final String EXAM_PROCTORING_REJON_ALL_COLLECTING_ROOM = "rejoin-all-collecting-room"; + public static final String SEB_CLIENT_CONNECTION_ENDPOINT = "/seb-client-connection"; public static final String SEB_CLIENT_EVENT_ENDPOINT = "/seb-client-event"; 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 d3173e70..8aac2f70 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 @@ -372,16 +372,25 @@ public class MonitoringClientConnection implements TemplateComposer { final String roomName = urlEncoder.encodeToString(Utils.toByteArray(connectionToken)); final String examId = action.getEntityKey().modelId; - final SEBProctoringConnectionData proctoringConnectionData = this.pageService.getCurrentUser() - .getProctoringGUIService() - .registerNewSingleProcotringRoom( - examId, - roomName, - connectionData.clientConnection.userSessionId, - connectionToken) - .getOrThrow(); + final ProctoringGUIService proctoringGUIService = this.pageService + .getCurrentUser() + .getProctoringGUIService(); + + if (!proctoringGUIService.hasRoom(roomName)) { + final SEBProctoringConnectionData proctoringConnectionData = proctoringGUIService + .registerNewSingleProcotringRoom( + examId, + roomName, + connectionData.clientConnection.userSessionId, + connectionToken) + .onError(error -> log.error( + "Failed to open single proctoring room for connection {} {}", + connectionToken, + error.getMessage())) + .getOr(null); + ProctoringGUIService.setCurrentProctoringWindowData(examId, proctoringConnectionData); + } - ProctoringGUIService.setCurrentProctoringWindowData(examId, proctoringConnectionData); final JavaScriptExecutor javaScriptExecutor = RWT.getClient().getService(JavaScriptExecutor.class); final String script = String.format( OPEN_SINGEL_ROOM_SCRIPT, @@ -389,9 +398,7 @@ public class MonitoringClientConnection implements TemplateComposer { this.guiServiceInfo.getExternalServerURIBuilder().toUriString(), this.remoteProctoringEndpoint); javaScriptExecutor.execute(script); - this.pageService.getCurrentUser() - .getProctoringGUIService() - .registerProctoringWindow(roomName); + proctoringGUIService.registerProctoringWindow(roomName); return action; } 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 74208d04..fbabfc59 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 @@ -108,6 +108,8 @@ public class MonitoringRunningExam implements TemplateComposer { new LocTextKey("sebserver.monitoring.exam.connection.action.instruction.quit.all.confirm"); private static final LocTextKey CONFIRM_DISABLE_SELECTED = new LocTextKey("sebserver.monitoring.exam.connection.action.instruction.disable.selected.confirm"); + private static final LocTextKey EXAM_ROOM_NAME = + new LocTextKey("sebserver.monitoring.exam.proctoring.room.all.name"); private final ServerPushService serverPushService; private final PageService pageService; @@ -329,6 +331,13 @@ public class MonitoringRunningExam implements TemplateComposer { .getOr(null); if (proctoringSettings != null && proctoringSettings.enableProctoring) { + + actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_CREATE_ALL_PROCTOR_ROOM) + .withEntityKey(entityKey) + .withExec(this::createCollectingAllRoom) + .noEventPropagation() + .publish(); + final Map> availableRooms = new HashMap<>(); updateRoomActions( entityKey, @@ -348,6 +357,41 @@ public class MonitoringRunningExam implements TemplateComposer { } } + private PageAction createCollectingAllRoom(final PageAction action) { + final EntityKey examId = action.getEntityKey(); + + final ProctoringGUIService proctoringGUIService = this.pageService + .getCurrentUser() + .getProctoringGUIService(); + + String activeAllRoomName = proctoringGUIService.getActiveAllRoom(examId.modelId); + + if (activeAllRoomName == null) { + final SEBProctoringConnectionData proctoringConnectionData = proctoringGUIService + .registerAllProcotringRoom( + examId.modelId, + this.pageService.getI18nSupport().getText(EXAM_ROOM_NAME)) + .onError(error -> log.error( + "Failed to open all collecting room for exam {} {}", examId.modelId, error.getMessage())) + .getOrThrow(); + ProctoringGUIService.setCurrentProctoringWindowData( + examId.modelId, + proctoringConnectionData); + activeAllRoomName = proctoringConnectionData.roomName; + } + + final JavaScriptExecutor javaScriptExecutor = RWT.getClient().getService(JavaScriptExecutor.class); + final String script = String.format( + OPEN_EXAM_COLLECTION_ROOM_SCRIPT, + activeAllRoomName, + this.guiServiceInfo.getExternalServerURIBuilder().toUriString(), + this.remoteProctoringEndpoint); + javaScriptExecutor.execute(script); + proctoringGUIService.registerProctoringWindow(activeAllRoomName); + + return action; + } + private void updateRoomActions( final EntityKey entityKey, final PageContext pageContext, @@ -359,7 +403,8 @@ public class MonitoringRunningExam implements TemplateComposer { this.pageService.getRestService().getBuilder(GetProcotringRooms.class) .withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId) .call() - .getOrThrow() + .onError(error -> log.error("Failed to update proctoring rooms on GUI {}", error.getMessage())) + .getOr(Collections.emptyList()) .stream() .forEach(room -> { if (rooms.containsKey(room.name)) { diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionDefinition.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionDefinition.java index 3d6b7640..e65682b6 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionDefinition.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionDefinition.java @@ -704,6 +704,11 @@ public enum ActionDefinition { ImageIcon.PROCTOR_ROOM, PageStateDefinitionImpl.MONITORING_RUNNING_EXAM, ActionCategory.PROCTORING), + MONITOR_EXAM_CREATE_ALL_PROCTOR_ROOM( + new LocTextKey("sebserver.monitoring.exam.action.proctoring.allRoom"), + ImageIcon.PROCTOR_ROOM, + PageStateDefinitionImpl.MONITORING_RUNNING_EXAM, + ActionCategory.PROCTORING), LOGS_USER_ACTIVITY_LIST( new LocTextKey("sebserver.logs.activity.userlogs"), diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/GetProctoringSettings.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/GetProctoringSettings.java index 8be04957..7a16bd22 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/GetProctoringSettings.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/GetProctoringSettings.java @@ -36,7 +36,7 @@ public class GetProctoringSettings extends RestCall { MediaType.APPLICATION_JSON_UTF8, API.EXAM_ADMINISTRATION_ENDPOINT + API.MODEL_ID_VAR_PATH_SEGMENT - + API.PROCTORING_PATH_SEGMENT); + + API.EXAM_ADMINISTRATION_PROCTORING_PATH_SEGMENT); } } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/SaveProctoringSettings.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/SaveProctoringSettings.java index d8bbf79f..e0249b68 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/SaveProctoringSettings.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/SaveProctoringSettings.java @@ -36,7 +36,7 @@ public class SaveProctoringSettings extends RestCall { MediaType.APPLICATION_JSON_UTF8, API.EXAM_ADMINISTRATION_ENDPOINT + API.MODEL_ID_VAR_PATH_SEGMENT - + API.PROCTORING_PATH_SEGMENT); + + API.EXAM_ADMINISTRATION_PROCTORING_PATH_SEGMENT); } } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/LeaveRemoteProctoringRoom.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/CreateCollectingAllProctoringRoom.java similarity index 73% rename from src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/LeaveRemoteProctoringRoom.java rename to src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/CreateCollectingAllProctoringRoom.java index b99770b6..580d362c 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/LeaveRemoteProctoringRoom.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/CreateCollectingAllProctoringRoom.java @@ -8,8 +8,6 @@ package ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session; -import java.util.List; - import org.springframework.context.annotation.Lazy; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; @@ -26,19 +24,19 @@ import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall; @Lazy @Component @GuiProfile -public class LeaveRemoteProctoringRoom extends RestCall> { +public class CreateCollectingAllProctoringRoom extends RestCall { - public LeaveRemoteProctoringRoom() { + public CreateCollectingAllProctoringRoom() { super(new TypeKey<>( CallType.UNDEFINED, EntityType.EXAM_PROCTOR_DATA, - new TypeReference>() { + new TypeReference() { }), HttpMethod.POST, MediaType.APPLICATION_FORM_URLENCODED, - API.EXAM_MONITORING_ENDPOINT + API.EXAM_PROCTORING_ENDPOINT + API.MODEL_ID_VAR_PATH_SEGMENT - + API.PROCTORING_PATH_SEGMENT - + API.PROCTORING_LEAVE_ROOM_PATH_SEGMENT); + + API.EXAM_PROCTORING_JON_ALL_COLLECTING_ROOM); } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/DisposeCollectingAllProctoringRoom.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/DisposeCollectingAllProctoringRoom.java new file mode 100644 index 00000000..3f2aaa0d --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/DisposeCollectingAllProctoringRoom.java @@ -0,0 +1,41 @@ +/* + * 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.gui.service.remote.webservice.api.session; + +import org.springframework.context.annotation.Lazy; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; + +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.profile.GuiProfile; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall; + +@Lazy +@Component +@GuiProfile +public class DisposeCollectingAllProctoringRoom extends RestCall { + + public DisposeCollectingAllProctoringRoom() { + super(new TypeKey<>( + CallType.UNDEFINED, + EntityType.EXAM_PROCTOR_DATA, + new TypeReference() { + }), + HttpMethod.POST, + MediaType.APPLICATION_FORM_URLENCODED, + API.EXAM_PROCTORING_ENDPOINT + + API.MODEL_ID_VAR_PATH_SEGMENT + + API.EXAM_PROCTORING_REJON_ALL_COLLECTING_ROOM); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/GetProcotringRooms.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/GetProcotringRooms.java index bf3280aa..2e147d8c 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/GetProcotringRooms.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/GetProcotringRooms.java @@ -36,9 +36,8 @@ public class GetProcotringRooms extends RestCall { }), HttpMethod.POST, MediaType.APPLICATION_FORM_URLENCODED, - API.EXAM_MONITORING_ENDPOINT + API.EXAM_PROCTORING_ENDPOINT + API.MODEL_ID_VAR_PATH_SEGMENT - + API.PROCTORING_PATH_SEGMENT - + API.PROCTORING_BROADCAST_OFF_PATH_SEGMENT); + + API.EXAM_PROCTORING_BROADCAST_OFF_PATH_SEGMENT); } } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/SendProctoringBroadcastOnInstruction.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/SendProctoringBroadcastOnInstruction.java index 3a5a484a..f8f1d726 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/SendProctoringBroadcastOnInstruction.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/SendProctoringBroadcastOnInstruction.java @@ -33,10 +33,9 @@ public class SendProctoringBroadcastOnInstruction extends RestCall { }), HttpMethod.POST, MediaType.APPLICATION_FORM_URLENCODED, - API.EXAM_MONITORING_ENDPOINT + API.EXAM_PROCTORING_ENDPOINT + API.MODEL_ID_VAR_PATH_SEGMENT - + API.PROCTORING_PATH_SEGMENT - + API.PROCTORING_BROADCAST_ON_PATH_SEGMENT); + + API.EXAM_PROCTORING_BROADCAST_ON_PATH_SEGMENT); } } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/SendRejoinExamCollectionRoom.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/SendRejoinExamCollectionRoom.java index dde6975a..96a23f59 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/SendRejoinExamCollectionRoom.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/SendRejoinExamCollectionRoom.java @@ -33,10 +33,9 @@ public class SendRejoinExamCollectionRoom extends RestCall { }), HttpMethod.POST, MediaType.APPLICATION_FORM_URLENCODED, - API.EXAM_MONITORING_ENDPOINT + API.EXAM_PROCTORING_ENDPOINT + API.MODEL_ID_VAR_PATH_SEGMENT - + API.PROCTORING_PATH_SEGMENT - + API.PROCTORING_REJOIN_EXAM_ROOM_PATH_SEGMENT); + + API.EXAM_PROCTORING_REJOIN_COLLECTING_ROOM_PATH_SEGMENT); } } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/session/ProctoringGUIService.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/ProctoringGUIService.java index da08e983..76160ed9 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/session/ProctoringGUIService.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/ProctoringGUIService.java @@ -14,6 +14,7 @@ import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; @@ -28,6 +29,8 @@ import ch.ethz.seb.sebserver.gbl.api.API; import ch.ethz.seb.sebserver.gbl.model.exam.SEBProctoringConnectionData; import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.CreateCollectingAllProctoringRoom; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.DisposeCollectingAllProctoringRoom; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.SendJoinRemoteProctoringRoom; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.SendRejoinExamCollectionRoom; @@ -84,26 +87,54 @@ public class ProctoringGUIService { new ProctoringWindowData(examId, data)); } + public boolean hasRoom(final String roomName) { + return this.rooms.containsKey(roomName); + } + + public String getActiveAllRoom(final String examId) { + return this.rooms + .values() + .stream() + .filter(data -> Objects.equals(data.examId, examId) && data.connections.isEmpty()) + .findFirst() + .map(data -> data.roomName) + .orElseGet(() -> null); + } + public Result registerNewSingleProcotringRoom( final String examId, final String roomName, final String subject, final String connectionToken) { - return Result.tryCatch(() -> { - final SEBProctoringConnectionData connection = - this.restService.getBuilder(SendJoinRemoteProctoringRoom.class) - .withURIVariable(API.PARAM_MODEL_ID, examId) - .withFormParam(SEBProctoringConnectionData.ATTR_ROOM_NAME, roomName) - .withFormParam(SEBProctoringConnectionData.ATTR_SUBJECT, subject) - .withFormParam(API.EXAM_API_SEB_CONNECTION_TOKEN, connectionToken) - .call() - .getOrThrow(); + return this.restService.getBuilder(SendJoinRemoteProctoringRoom.class) + .withURIVariable(API.PARAM_MODEL_ID, examId) + .withFormParam(SEBProctoringConnectionData.ATTR_ROOM_NAME, roomName) + .withFormParam(SEBProctoringConnectionData.ATTR_SUBJECT, subject) + .withFormParam(API.EXAM_API_SEB_CONNECTION_TOKEN, connectionToken) + .call() + .map(connection -> { + this.rooms.put(roomName, new RoomConnectionData(roomName, examId, connectionToken)); + this.openWindows.add(roomName); + return connection; + }); + } - this.rooms.put(roomName, new RoomConnectionData(roomName, examId, connectionToken)); - this.openWindows.add(roomName); - return connection; - }); + public Result registerAllProcotringRoom( + final String examId, + final String subject) { + + return this.restService.getBuilder(CreateCollectingAllProctoringRoom.class) + .withURIVariable(API.PARAM_MODEL_ID, examId) + .withFormParam(SEBProctoringConnectionData.ATTR_SUBJECT, subject) + .call() + .map(connection -> { + this.rooms.put( + connection.roomName, + new RoomConnectionData(connection.roomName, examId)); + this.openWindows.add(connection.roomName); + return connection; + }); } public Result registerNewProcotringRoom( @@ -112,22 +143,19 @@ public class ProctoringGUIService { final String subject, final Collection connectionTokens) { - return Result.tryCatch(() -> { - final SEBProctoringConnectionData connection = - this.restService.getBuilder(SendJoinRemoteProctoringRoom.class) - .withURIVariable(API.PARAM_MODEL_ID, examId) - .withFormParam(SEBProctoringConnectionData.ATTR_ROOM_NAME, roomName) - .withFormParam(SEBProctoringConnectionData.ATTR_SUBJECT, subject) - .withFormParam( - API.EXAM_API_SEB_CONNECTION_TOKEN, - StringUtils.join(connectionTokens, Constants.LIST_SEPARATOR_CHAR)) - .call() - .getOrThrow(); - - this.rooms.put(roomName, new RoomConnectionData(roomName, examId, connectionTokens)); - this.openWindows.add(roomName); - return connection; - }); + return this.restService.getBuilder(SendJoinRemoteProctoringRoom.class) + .withURIVariable(API.PARAM_MODEL_ID, examId) + .withFormParam(SEBProctoringConnectionData.ATTR_ROOM_NAME, roomName) + .withFormParam(SEBProctoringConnectionData.ATTR_SUBJECT, subject) + .withFormParam( + API.EXAM_API_SEB_CONNECTION_TOKEN, + StringUtils.join(connectionTokens, Constants.LIST_SEPARATOR_CHAR)) + .call() + .map(connection -> { + this.rooms.put(roomName, new RoomConnectionData(roomName, examId, connectionTokens)); + this.openWindows.add(roomName); + return connection; + }); } public void addConnectionsToRoom( @@ -147,7 +175,9 @@ public class ProctoringGUIService { API.EXAM_API_SEB_CONNECTION_TOKEN, StringUtils.join(connectionTokens, Constants.LIST_SEPARATOR_CHAR)) .call() - .getOrThrow(); + .onError(error -> log.error("Failed to add connection to proctoring room: {} {}", + room, + error.getMessage())); roomConnectionData.connections.addAll(connectionTokens); } } @@ -164,15 +194,24 @@ public class ProctoringGUIService { final RoomConnectionData roomConnectionData = this.rooms.remove(name); if (roomConnectionData != null) { // send instruction to leave this room and join the own exam collection room - - this.restService.getBuilder(SendRejoinExamCollectionRoom.class) - .withURIVariable(API.PARAM_MODEL_ID, roomConnectionData.examId) - .withFormParam( - API.EXAM_API_SEB_CONNECTION_TOKEN, - StringUtils.join(roomConnectionData.connections, Constants.LIST_SEPARATOR_CHAR)) - .call() - .getOrThrow(); - + if (!roomConnectionData.connections.isEmpty()) { + this.restService.getBuilder(SendRejoinExamCollectionRoom.class) + .withURIVariable(API.PARAM_MODEL_ID, roomConnectionData.examId) + .withFormParam( + API.EXAM_API_SEB_CONNECTION_TOKEN, + StringUtils.join(roomConnectionData.connections, Constants.LIST_SEPARATOR_CHAR)) + .call() + .onError(error -> log.error("Failed to close proctoring room: {} {}", + name, + error.getMessage())); + } else { + this.restService.getBuilder(DisposeCollectingAllProctoringRoom.class) + .withURIVariable(API.PARAM_MODEL_ID, roomConnectionData.examId) + .call() + .onError(error -> log.error("Failed to close proctoring room: {} {}", + name, + error.getMessage())); + } } } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamProctoringService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamProctoringService.java index 832f4069..d26bfc8f 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamProctoringService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamProctoringService.java @@ -11,6 +11,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.session; import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings; import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings.ProctoringServerType; import ch.ethz.seb.sebserver.gbl.model.exam.SEBProctoringConnectionData; +import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; import ch.ethz.seb.sebserver.gbl.util.Result; public interface ExamProctoringService { @@ -20,14 +21,18 @@ public interface ExamProctoringService { Result testExamProctoring(final ProctoringSettings examProctoring); Result createProctorPublicRoomConnection( - final ProctoringSettings examProctoring, + final ProctoringSettings proctoringSettings, final String roomName, final String subject); Result getClientExamCollectionRoomConnectionData( - final ProctoringSettings examProctoring, + final ProctoringSettings proctoringSettings, final String connectionToken); + Result getClientExamCollectionRoomConnectionData( + final ProctoringSettings proctoringSettings, + final ClientConnection connection); + Result getClientExamCollectionRoomConnectionData( final ProctoringSettings proctoringSettings, final String connectionToken, diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamJITSIProctoringService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamJITSIProctoringService.java index ffc1fa57..45fbd1c1 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamJITSIProctoringService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamJITSIProctoringService.java @@ -13,6 +13,7 @@ import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.Base64; import java.util.Base64.Encoder; +import java.util.Collection; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; @@ -26,6 +27,7 @@ import ch.ethz.seb.sebserver.gbl.model.exam.Exam; import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings; import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings.ProctoringServerType; import ch.ethz.seb.sebserver.gbl.model.exam.SEBProctoringConnectionData; +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.RemoteProctoringRoom; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; @@ -99,15 +101,27 @@ public class ExamJITSIProctoringService implements ExamProctoringService { final ProctoringSettings proctoringSettings, final String connectionToken) { + return this.examSessionService + .getConnectionData(connectionToken) + .flatMap(connection -> getClientExamCollectionRoomConnectionData( + proctoringSettings, + connection.clientConnection)); + } + + @Override + public Result getClientExamCollectionRoomConnectionData( + final ProctoringSettings proctoringSettings, + final ClientConnection connection) { + return Result.tryCatch(() -> { - final ClientConnectionData clientConnection = this.examSessionService - .getConnectionData(connectionToken) - .getOrThrow(); - final RemoteProctoringRoom room = this.examSessionService + + final Collection remoteProctoringRooms = this.examSessionService .getExamSessionCacheService() - .getRemoteProctoringRooms(clientConnection.clientConnection.examId) + .getRemoteProctoringRooms(connection.examId); + + final RemoteProctoringRoom room = remoteProctoringRooms .stream() - .filter(r -> r.id.equals(clientConnection.clientConnection.remoteProctoringRoomId)) + .filter(r -> r.id.equals(connection.remoteProctoringRoomId)) .findFirst() .orElseGet(() -> { throw new RuntimeException("No exam proctoring room found for clientConnection"); @@ -119,10 +133,10 @@ public class ExamJITSIProctoringService implements ExamProctoringService { proctoringSettings.serverURL, proctoringSettings.appKey, proctoringSettings.getAppSecret(), - clientConnection.clientConnection.userSessionId, + connection.userSessionId, "seb-client", room.name, - clientConnection.clientConnection.userSessionId, + connection.userSessionId, forExam(proctoringSettings)) .getOrThrow(); }); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamProcotringRoomServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamProcotringRoomServiceImpl.java index 4627c0dc..2872d348 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamProcotringRoomServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamProcotringRoomServiceImpl.java @@ -124,7 +124,8 @@ public class ExamProcotringRoomServiceImpl implements ExamProcotringRoomService .build() .execute(); - flagUpdated(toUpdate).stream() + flagUpdated(toUpdate) + .stream() .forEach(cc -> { if (ConnectionStatus.ACTIVE.name().equals(cc.getStatus())) { assignToRoom(cc); @@ -156,13 +157,14 @@ public class ExamProcotringRoomServiceImpl implements ExamProcotringRoomService ACTIVE_COLLECTING_ALL_ROOM_ATTRIBUTE_NAME); } - private Result getActiveCollectingAllRoom(final Long examId) { + private String getActiveCollectingAllRoom(final Long examId) { return this.additionalAttributesDAO .getAdditionalAttribute( EntityType.EXAM, examId, ACTIVE_COLLECTING_ALL_ROOM_ATTRIBUTE_NAME) - .map(attr -> attr.getValue()); + .map(attr -> attr.getValue()) + .getOr(null); } // TODO considering doing bulk update here @@ -202,7 +204,13 @@ public class ExamProcotringRoomServiceImpl implements ExamProcotringRoomService proctoringRoom.id, 0)); this.examSessionCacheService.evictRemoteProctoringRooms(cc.getExamId()); - applyProcotringInstruction(cc.getExamId(), cc.getConnectionToken(), proctoringRoom.name); + this.examSessionCacheService.evictClientConnection(cc.getConnectionToken()); + final String activeCollectingAllRoom = getActiveCollectingAllRoom(cc.getExamId()); + if (activeCollectingAllRoom != null) { + applyProcotringInstruction(cc.getExamId(), cc.getConnectionToken(), activeCollectingAllRoom); + } else { + applyProcotringInstruction(cc.getExamId(), cc.getConnectionToken(), proctoringRoom.name); + } } } catch (final Exception e) { log.error("Failed to process proctoring room update for client connection: {}", cc.getConnectionToken(), e); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java index 4927e3d1..7b44fe03 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java @@ -21,7 +21,6 @@ import java.util.stream.Collectors; import org.apache.commons.lang3.BooleanUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; import org.springframework.context.annotation.Lazy; import org.springframework.security.access.AccessDeniedException; @@ -314,9 +313,15 @@ public class ExamSessionServiceImpl implements ExamSessionService { @Override public Result getConnectionData(final String connectionToken) { + return Result.tryCatch(() -> { - final Cache cache = this.cacheManager.getCache(ExamSessionCacheService.CACHE_NAME_ACTIVE_CLIENT_CONNECTION); - return cache.get(connectionToken, ClientConnectionData.class); + final ClientConnectionDataInternal activeClientConnection = this.examSessionCacheService + .getActiveClientConnection(connectionToken); + if (activeClientConnection == null) { + throw new NoSuchElementException("Client Connection with token: " + connectionToken); + } + + return activeClientConnection; }); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java index 162249c1..ee1e281f 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java @@ -383,7 +383,7 @@ public class ExamAdministrationController extends EntityController { @RequestMapping( path = API.MODEL_ID_VAR_PATH_SEGMENT - + API.PROCTORING_PATH_SEGMENT, + + API.EXAM_ADMINISTRATION_PROCTORING_PATH_SEGMENT, method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) public ProctoringSettings getExamProctoring( @@ -402,7 +402,7 @@ public class ExamAdministrationController extends EntityController { @RequestMapping( path = API.MODEL_ID_VAR_PATH_SEGMENT - + API.PROCTORING_PATH_SEGMENT, + + API.EXAM_ADMINISTRATION_PROCTORING_PATH_SEGMENT, method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) public Exam saveExamProctoring( 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 6842d395..e4f143cc 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 @@ -9,19 +9,14 @@ package ch.ethz.seb.sebserver.webservice.weblayer.api; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.EnumSet; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.Objects; -import java.util.stream.Collectors; import javax.servlet.http.HttpServletRequest; import javax.validation.Valid; -import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -44,25 +39,16 @@ import ch.ethz.seb.sebserver.gbl.api.authorization.PrivilegeType; import ch.ethz.seb.sebserver.gbl.model.Domain; import ch.ethz.seb.sebserver.gbl.model.Page; import ch.ethz.seb.sebserver.gbl.model.exam.Exam; -import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings; -import ch.ethz.seb.sebserver.gbl.model.exam.SEBProctoringConnectionData; -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.ClientInstruction; -import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction.InstructionType; -import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom; import ch.ethz.seb.sebserver.gbl.model.user.UserRole; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; -import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.webservice.servicelayer.PaginationService; import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.AuthorizationService; import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.PermissionDeniedException; 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.exam.ExamAdminService; -import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamProcotringRoomService; -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.SEBClientConnectionService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBInstructionService; @@ -76,27 +62,21 @@ public class ExamMonitoringController { private final SEBClientConnectionService sebClientConnectionService; private final ExamSessionService examSessionService; - private final ExamAdminService examAdminService; private final SEBInstructionService sebInstructionService; private final AuthorizationService authorization; private final PaginationService paginationService; - private final ExamProcotringRoomService examProcotringRoomService; public ExamMonitoringController( - final ExamAdminService examAdminService, final SEBClientConnectionService sebClientConnectionService, final SEBInstructionService sebInstructionService, final AuthorizationService authorization, - final PaginationService paginationService, - final ExamProcotringRoomService examProcotringRoomService) { + final PaginationService paginationService) { - this.examAdminService = examAdminService; this.sebClientConnectionService = sebClientConnectionService; this.examSessionService = sebClientConnectionService.getExamSessionService(); this.sebInstructionService = sebInstructionService; this.authorization = authorization; this.paginationService = paginationService; - this.examProcotringRoomService = examProcotringRoomService; } /** This is called by Spring to initialize the WebDataBinder and is used here to @@ -289,387 +269,6 @@ public class ExamMonitoringController { } - //*********************************************************************************************** - //**** Proctoring - - @RequestMapping( - path = API.MODEL_ID_VAR_PATH_SEGMENT - + API.PROCTORING_PATH_SEGMENT - + API.PROCTORING_ROOMS_SEGMENT, - method = RequestMethod.GET, - consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, - produces = MediaType.APPLICATION_JSON_UTF8_VALUE) - public Collection getDefaultProcotringRoomsOfExam(@RequestParam( - name = API.PARAM_INSTITUTION_ID, - required = true, - defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId, - @PathVariable(name = API.PARAM_MODEL_ID) final Long examId) { - - return this.examProcotringRoomService - .getProctoringRooms(examId) - .getOrThrow(); - } - - @RequestMapping( - path = API.MODEL_ID_VAR_PATH_SEGMENT - + API.PROCTORING_PATH_SEGMENT, - method = RequestMethod.GET, - consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, - produces = MediaType.APPLICATION_JSON_UTF8_VALUE) - public SEBProctoringConnectionData getProctorRoomData( - @RequestParam( - name = API.PARAM_INSTITUTION_ID, - required = true, - defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId, - @PathVariable(name = API.PARAM_MODEL_ID) final Long examId, - @RequestParam(name = SEBProctoringConnectionData.ATTR_ROOM_NAME, required = true) final String roomName, - @RequestParam(name = SEBProctoringConnectionData.ATTR_SUBJECT, required = false) final String subject) { - - this.authorization.check( - PrivilegeType.READ, - EntityType.EXAM, - institutionId); - - this.authorization.checkRead( - this.examSessionService.getExamDAO().byPK(examId).getOrThrow()); - - return this.examSessionService.getRunningExam(examId) - .flatMap(this.authorization::checkRead) - .flatMap(this.examAdminService::getExamProctoring) - .flatMap(proc -> this.examAdminService - .getExamProctoringService(proc.serverType) - .flatMap(s -> s.createProctorPublicRoomConnection( - proc, - roomName, - StringUtils.isNoneBlank(subject) ? subject : roomName))) - .getOrThrow(); - } - - @RequestMapping( - path = API.MODEL_ID_VAR_PATH_SEGMENT - + API.PROCTORING_PATH_SEGMENT - + API.PROCTORING_ROOM_CONNECTIONS_PATH_SEGMENT, - method = RequestMethod.GET, - consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, - produces = MediaType.APPLICATION_JSON_UTF8_VALUE) - public Collection getProctorRoomConnectionData( - @RequestParam( - name = API.PARAM_INSTITUTION_ID, - required = true, - defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId, - @PathVariable(name = API.PARAM_MODEL_ID) final Long examId, - @RequestParam( - name = Domain.REMOTE_PROCTORING_ROOM.ATTR_ID, - required = true) final String roomName) { - - this.authorization.check( - PrivilegeType.READ, - EntityType.EXAM, - institutionId); - - this.authorization.checkRead( - this.examSessionService.getExamDAO().byPK(examId).getOrThrow()); - - return this.examProcotringRoomService.getRoomConnections(examId, roomName) - .getOrThrow(); - } - - @RequestMapping( - path = API.MODEL_ID_VAR_PATH_SEGMENT - + API.PROCTORING_PATH_SEGMENT - + API.PROCTORING_BROADCAST_ON_PATH_SEGMENT, - method = RequestMethod.POST, - produces = MediaType.APPLICATION_JSON_UTF8_VALUE) - public void sendBroadcastOn( - @RequestParam( - name = API.PARAM_INSTITUTION_ID, - required = true, - defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId, - @PathVariable(name = API.PARAM_MODEL_ID) final Long examId, - @RequestParam( - name = Domain.REMOTE_PROCTORING_ROOM.ATTR_ID, - required = false) final String roomName, - @RequestParam( - name = API.EXAM_API_SEB_CONNECTION_TOKEN, - required = false) final String connectionTokens, - @RequestParam( - name = ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_RECONFIGURE_SETTINGS.JITSI_RECEIVE_AUDIO, - required = false) final Boolean sendReceiveAudio, - @RequestParam( - name = ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_RECONFIGURE_SETTINGS.JITSI_RECEIVE_VIDEO, - required = false) final Boolean sendReceiveVideo, - @RequestParam( - name = ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_RECONFIGURE_SETTINGS.JITSI_ALLOW_CHAT, - required = false) final Boolean sendAllowChat) { - - this.authorization.check( - PrivilegeType.READ, - EntityType.EXAM, - institutionId); - - this.authorization.checkRead( - this.examSessionService.getExamDAO().byPK(examId).getOrThrow()); - - final Map attributes = createProctorInstructionAttributes( - sendReceiveAudio, - sendReceiveVideo, - sendAllowChat, - Constants.TRUE_STRING); - - sendProctoringInstructions(examId, roomName, connectionTokens, attributes); - } - - @RequestMapping( - path = API.MODEL_ID_VAR_PATH_SEGMENT - + API.PROCTORING_PATH_SEGMENT - + API.PROCTORING_BROADCAST_OFF_PATH_SEGMENT, - method = RequestMethod.POST, - produces = MediaType.APPLICATION_JSON_UTF8_VALUE) - public void sendBroadcastOff( - @RequestParam( - name = API.PARAM_INSTITUTION_ID, - required = true, - defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId, - @PathVariable(name = API.PARAM_MODEL_ID) final Long examId, - @RequestParam( - name = Domain.REMOTE_PROCTORING_ROOM.ATTR_ID, - required = true) final String roomName, - @RequestParam( - name = API.EXAM_API_SEB_CONNECTION_TOKEN, - required = true) final String connectionTokens, - @RequestParam( - name = ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_RECONFIGURE_SETTINGS.JITSI_RECEIVE_AUDIO, - required = false) final Boolean sendReceiveAudio, - @RequestParam( - name = ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_RECONFIGURE_SETTINGS.JITSI_RECEIVE_VIDEO, - required = false) final Boolean sendReceiveVideo, - @RequestParam( - name = ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_RECONFIGURE_SETTINGS.JITSI_ALLOW_CHAT, - required = false) final Boolean sendAllowChat) { - - this.authorization.check( - PrivilegeType.READ, - EntityType.EXAM, - institutionId); - - this.authorization.checkRead( - this.examSessionService.getExamDAO().byPK(examId).getOrThrow()); - - final Map attributes = createProctorInstructionAttributes( - sendReceiveAudio, - sendReceiveVideo, - sendAllowChat, - Constants.FALSE_STRING); - - if (attributes.isEmpty()) { - log.warn("Missing reconfigure instruction attributes. Skip sending empty instruction to SEB clients"); - return; - } - - sendProctoringInstructions(examId, roomName, connectionTokens, attributes); - } - - @RequestMapping( - path = API.MODEL_ID_VAR_PATH_SEGMENT - + API.PROCTORING_PATH_SEGMENT - + API.PROCTORING_REJOIN_EXAM_ROOM_PATH_SEGMENT, - method = RequestMethod.POST, - produces = MediaType.APPLICATION_JSON_UTF8_VALUE) - public void sendRejoinExamCollectionRoomToClients( - @RequestParam( - name = API.PARAM_INSTITUTION_ID, - required = true, - defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId, - @PathVariable(name = API.PARAM_MODEL_ID) final Long examId, - @RequestParam( - name = API.EXAM_API_SEB_CONNECTION_TOKEN, - required = true) final String connectionTokens) { - - this.authorization.check( - PrivilegeType.READ, - EntityType.EXAM, - institutionId); - - this.authorization.checkRead( - this.examSessionService.getExamDAO().byPK(examId).getOrThrow()); - - final ProctoringSettings settings = this.examSessionService - .getRunningExam(examId) - .flatMap(this.authorization::checkRead) - .flatMap(this.examAdminService::getExamProctoring) - .getOrThrow(); - - final ExamProctoringService examProctoringService = this.examAdminService - .getExamProctoringService(settings.serverType) - .getOrThrow(); - - Arrays.asList(StringUtils.split(connectionTokens, Constants.LIST_SEPARATOR)) - .stream() - .forEach(connectionToken -> { - final Result result = examProctoringService - .getClientExamCollectionRoomConnectionData( - settings, - connectionToken) - .flatMap(data -> this.sendJoinInstruction(examId, connectionTokens, data)); - if (result.hasError()) { - // TODO log - } - }); - } - - @RequestMapping( - path = API.MODEL_ID_VAR_PATH_SEGMENT - + API.PROCTORING_PATH_SEGMENT - + API.PROCTORING_JOIN_ROOM_PATH_SEGMENT, - method = RequestMethod.POST, - produces = MediaType.APPLICATION_JSON_UTF8_VALUE) - public SEBProctoringConnectionData sendJoinProctoringRoomToClients( - @RequestParam( - name = API.PARAM_INSTITUTION_ID, - required = true, - defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId, - @PathVariable(name = API.PARAM_MODEL_ID) final Long examId, - @RequestParam( - name = SEBProctoringConnectionData.ATTR_ROOM_NAME, - required = true) final String roomName, - @RequestParam( - name = SEBProctoringConnectionData.ATTR_SUBJECT, - required = false) final String subject, - @RequestParam( - name = API.EXAM_API_SEB_CONNECTION_TOKEN, - required = true) final String connectionTokens) { - - this.authorization.check( - PrivilegeType.READ, - EntityType.EXAM, - institutionId); - - this.authorization.checkRead( - this.examSessionService.getExamDAO().byPK(examId).getOrThrow()); - - final ProctoringSettings settings = this.examSessionService - .getRunningExam(examId) - .flatMap(this.authorization::checkRead) - .flatMap(this.examAdminService::getExamProctoring) - .getOrThrow(); - - final ExamProctoringService examProctoringService = this.examAdminService - .getExamProctoringService(settings.serverType) - .getOrThrow(); - - if (StringUtils.isNotBlank(connectionTokens)) { - final boolean single = connectionTokens.contains(Constants.LIST_SEPARATOR); - (single - ? Arrays.asList(StringUtils.split(connectionTokens, Constants.LIST_SEPARATOR)) - : Arrays.asList(connectionTokens)) - .stream() - .map(connectionToken -> { - final SEBProctoringConnectionData data = (single) - ? examProctoringService - .getClientRoomConnectionData(settings, connectionToken) - .getOrThrow() - : examProctoringService - .getClientRoomConnectionData( - settings, - connectionToken, - roomName, - (StringUtils.isNotBlank(subject)) ? subject : roomName) - .getOrThrow(); - sendJoinInstruction(examId, connectionToken, data) - .onError(error -> log.error( - "Failed to send proctoring leave instruction to client: {} ", - connectionToken, error)); - return data; - }).collect(Collectors.toList()); - } - - return examProctoringService.createProctorPublicRoomConnection( - settings, - roomName, - (StringUtils.isNotBlank(subject)) ? subject : roomName) - .getOrThrow(); - } - - private void sendProctoringInstructions( - final Long examId, - final String roomName, - final String connectionTokens, - final Map attributes) { - - if (attributes.isEmpty()) { - log.warn("Missing reconfigure instruction attributes. Skip sending empty instruction to SEB clients"); - return; - } - - if (StringUtils.isNotBlank(connectionTokens)) { - final boolean single = connectionTokens.contains(Constants.LIST_SEPARATOR); - (single - ? Arrays.asList(StringUtils.split(connectionTokens, Constants.LIST_SEPARATOR)) - : Arrays.asList(connectionTokens)) - .stream() - .forEach(connectionToken -> { - this.sebInstructionService.registerInstruction( - examId, - InstructionType.SEB_RECONFIGURE_SETTINGS, - attributes, - connectionToken, - true) - .onError(error -> log.error( - "Failed to register reconfiguring instruction for connection: {}", - connectionToken, - error)); - - }); - } else if (StringUtils.isNotBlank(roomName)) { - this.examProcotringRoomService.getRoomConnections(examId, roomName) - .getOrThrow() - .stream() - .forEach(connection -> { - this.sebInstructionService.registerInstruction( - examId, - InstructionType.SEB_RECONFIGURE_SETTINGS, - attributes, - connection.connectionToken, - true) - .onError(error -> log.error( - "Failed to register reconfiguring instruction for connection: {}", - connection.connectionToken, - error)); - }); - } else { - throw new RuntimeException("API attribute validation error: missing " - + Domain.REMOTE_PROCTORING_ROOM.ATTR_ID + " and/or" + - API.EXAM_API_SEB_CONNECTION_TOKEN + " attribute"); - } - } - - private Map createProctorInstructionAttributes( - final Boolean sendReceiveAudio, - final Boolean sendReceiveVideo, - final Boolean sendAllowChat, - final String flagValue) { - final Map attributes = new HashMap<>(); - if (BooleanUtils.isTrue(sendReceiveAudio)) { - attributes.put( - ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_RECONFIGURE_SETTINGS.JITSI_RECEIVE_AUDIO, - flagValue); - } - if (BooleanUtils.isTrue(sendReceiveVideo)) { - attributes.put( - ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_RECONFIGURE_SETTINGS.JITSI_RECEIVE_VIDEO, - flagValue); - } - if (BooleanUtils.isTrue(sendAllowChat)) { - attributes.put( - ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_RECONFIGURE_SETTINGS.JITSI_ALLOW_CHAT, - flagValue); - } - return attributes; - } - - //**** Proctoring - //*********************************************************************************************** - private boolean hasRunningExamPrivilege(final Long examId, final Long institution) { return hasRunningExamPrivilege( this.examSessionService.getRunningExam(examId).getOr(null), @@ -685,53 +284,4 @@ public class ExamMonitoringController { return exam.institutionId.equals(institution) && exam.isOwner(userId); } - private Result sendJoinInstruction( - final Long examId, - final String connectionToken, - final SEBProctoringConnectionData data) { - - final Map attributes = new HashMap<>(); - attributes.put( - ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.SERVICE_TYPE, - ProctoringSettings.ProctoringServerType.JITSI_MEET.name()); - attributes.put( - ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.METHOD, - ClientInstruction.ProctoringInstructionMethod.JOIN.name()); - attributes.put( - ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.JITSI_URL, - data.serverURL); - attributes.put( - ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.JITSI_ROOM, - data.roomName); - attributes.put( - ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.JITSI_TOKEN, - data.accessToken); - return this.sebInstructionService.registerInstruction( - examId, - InstructionType.SEB_PROCTORING, - attributes, - connectionToken, - true); - } - -// private Result sendLeaveInstruction( -// final Long examId, -// final String connectionToken, -// final SEBProctoringConnectionData data) { -// -// return sendProctorInstruction( -// examId, -// connectionToken, -// data, -// ClientInstruction.ProctoringInstructionMethod.LEAVE.name()); -// } - -// PRIVATE RESULT SENDPROCTORINSTRUCTION( -// FINAL LONG EXAMID, -// FINAL STRING CONNECTIONTOKEN, -// FINAL SEBPROCTORINGCONNECTIONDATA DATA, -// FINAL STRING METHOD) { -// -// } - } 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 new file mode 100644 index 00000000..ba3f05dd --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamProctoringController.java @@ -0,0 +1,587 @@ +/* + * 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.weblayer.api; + +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.MediaType; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.annotation.InitBinder; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import ch.ethz.seb.sebserver.gbl.Constants; +import ch.ethz.seb.sebserver.gbl.api.API; +import ch.ethz.seb.sebserver.gbl.api.EntityType; +import ch.ethz.seb.sebserver.gbl.api.authorization.PrivilegeType; +import ch.ethz.seb.sebserver.gbl.model.Domain; +import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings; +import ch.ethz.seb.sebserver.gbl.model.exam.SEBProctoringConnectionData; +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.ClientInstruction; +import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction.InstructionType; +import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom; +import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; +import ch.ethz.seb.sebserver.gbl.util.Result; +import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.AuthorizationService; +import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.UserService; +import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamAdminService; +import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamProcotringRoomService; +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; + +@WebServiceProfile +@RestController +@RequestMapping("${sebserver.webservice.api.admin.endpoint}" + API.EXAM_PROCTORING_ENDPOINT) +public class ExamProctoringController { + + private static final Logger log = LoggerFactory.getLogger(ExamProctoringController.class); + + private final ExamProcotringRoomService examProcotringRoomService; + private final ExamAdminService examAdminService; + private final SEBInstructionService sebInstructionService; + private final AuthorizationService authorization; + private final ExamSessionService examSessionService; + + public ExamProctoringController( + final ExamProcotringRoomService examProcotringRoomService, + final ExamAdminService examAdminService, + final SEBInstructionService sebInstructionService, + final AuthorizationService authorization, + final ExamSessionService examSessionService) { + + this.examProcotringRoomService = examProcotringRoomService; + this.examAdminService = examAdminService; + this.sebInstructionService = sebInstructionService; + this.authorization = authorization; + this.examSessionService = examSessionService; + } + + /** This is called by Spring to initialize the WebDataBinder and is used here to + * initialize the default value binding for the institutionId request-parameter + * that has the current users insitutionId as default. + * + * See also UserService.addUsersInstitutionDefaultPropertySupport */ + @InitBinder + public void initBinder(final WebDataBinder binder) { + this.authorization + .getUserService() + .addUsersInstitutionDefaultPropertySupport(binder); + } + + @RequestMapping( + path = API.MODEL_ID_VAR_PATH_SEGMENT + + API.EXAM_PROCTORING_ROOMS_SEGMENT, + method = RequestMethod.GET, + consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, + produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public Collection getDefaultProcotringRoomsOfExam( + @RequestParam( + name = API.PARAM_INSTITUTION_ID, + required = true, + defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId, + @PathVariable(name = API.PARAM_MODEL_ID) final Long examId) { + + return this.examProcotringRoomService + .getProctoringRooms(examId) + .getOrThrow(); + } + + @RequestMapping( + path = API.MODEL_ID_VAR_PATH_SEGMENT, + method = RequestMethod.GET, + consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, + produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public SEBProctoringConnectionData getProctorRoomData( + @RequestParam( + name = API.PARAM_INSTITUTION_ID, + required = true, + defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId, + @PathVariable(name = API.PARAM_MODEL_ID) final Long examId, + @RequestParam(name = SEBProctoringConnectionData.ATTR_ROOM_NAME, required = true) final String roomName, + @RequestParam(name = SEBProctoringConnectionData.ATTR_SUBJECT, required = false) final String subject) { + + checkAccess(institutionId, examId); + + return this.examSessionService.getRunningExam(examId) + .flatMap(this.authorization::checkRead) + .flatMap(this.examAdminService::getExamProctoring) + .flatMap(proc -> this.examAdminService + .getExamProctoringService(proc.serverType) + .flatMap(s -> s.createProctorPublicRoomConnection( + proc, + roomName, + StringUtils.isNoneBlank(subject) ? subject : roomName))) + .getOrThrow(); + } + + @RequestMapping( + path = API.MODEL_ID_VAR_PATH_SEGMENT + + API.EXAM_PROCTORING_ROOM_CONNECTIONS_PATH_SEGMENT, + method = RequestMethod.GET, + consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, + produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public Collection getProctorRoomConnectionData( + @RequestParam( + name = API.PARAM_INSTITUTION_ID, + required = true, + defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId, + @PathVariable(name = API.PARAM_MODEL_ID) final Long examId, + @RequestParam( + name = Domain.REMOTE_PROCTORING_ROOM.ATTR_ID, + required = true) final String roomName) { + + checkAccess(institutionId, examId); + + return this.examProcotringRoomService.getRoomConnections(examId, roomName) + .getOrThrow(); + } + + @RequestMapping( + path = API.MODEL_ID_VAR_PATH_SEGMENT + + API.EXAM_PROCTORING_BROADCAST_ON_PATH_SEGMENT, + method = RequestMethod.POST, + produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public void sendBroadcastOn( + @RequestParam( + name = API.PARAM_INSTITUTION_ID, + required = true, + defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId, + @PathVariable(name = API.PARAM_MODEL_ID) final Long examId, + @RequestParam( + name = Domain.REMOTE_PROCTORING_ROOM.ATTR_ID, + required = false) final String roomName, + @RequestParam( + name = API.EXAM_API_SEB_CONNECTION_TOKEN, + required = false) final String connectionTokens, + @RequestParam( + name = ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_RECONFIGURE_SETTINGS.JITSI_RECEIVE_AUDIO, + required = false) final Boolean sendReceiveAudio, + @RequestParam( + name = ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_RECONFIGURE_SETTINGS.JITSI_RECEIVE_VIDEO, + required = false) final Boolean sendReceiveVideo, + @RequestParam( + name = ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_RECONFIGURE_SETTINGS.JITSI_ALLOW_CHAT, + required = false) final Boolean sendAllowChat) { + + checkAccess(institutionId, examId); + + final Map attributes = createProctorInstructionAttributes( + sendReceiveAudio, + sendReceiveVideo, + sendAllowChat, + Constants.TRUE_STRING); + + sendProctoringInstructions(examId, roomName, connectionTokens, attributes); + } + + @RequestMapping( + path = API.MODEL_ID_VAR_PATH_SEGMENT + + API.EXAM_PROCTORING_BROADCAST_OFF_PATH_SEGMENT, + method = RequestMethod.POST, + produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public void sendBroadcastOff( + @RequestParam( + name = API.PARAM_INSTITUTION_ID, + required = true, + defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId, + @PathVariable(name = API.PARAM_MODEL_ID) final Long examId, + @RequestParam( + name = Domain.REMOTE_PROCTORING_ROOM.ATTR_ID, + required = true) final String roomName, + @RequestParam( + name = API.EXAM_API_SEB_CONNECTION_TOKEN, + required = true) final String connectionTokens, + @RequestParam( + name = ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_RECONFIGURE_SETTINGS.JITSI_RECEIVE_AUDIO, + required = false) final Boolean sendReceiveAudio, + @RequestParam( + name = ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_RECONFIGURE_SETTINGS.JITSI_RECEIVE_VIDEO, + required = false) final Boolean sendReceiveVideo, + @RequestParam( + name = ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_RECONFIGURE_SETTINGS.JITSI_ALLOW_CHAT, + required = false) final Boolean sendAllowChat) { + + checkAccess(institutionId, examId); + + final Map attributes = createProctorInstructionAttributes( + sendReceiveAudio, + sendReceiveVideo, + sendAllowChat, + Constants.FALSE_STRING); + + if (attributes.isEmpty()) { + log.warn("Missing reconfigure instruction attributes. Skip sending empty instruction to SEB clients"); + return; + } + + sendProctoringInstructions(examId, roomName, connectionTokens, attributes); + } + + @RequestMapping( + path = API.MODEL_ID_VAR_PATH_SEGMENT + + API.EXAM_PROCTORING_REJOIN_COLLECTING_ROOM_PATH_SEGMENT, + method = RequestMethod.POST, + produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public void sendRejoinExamCollectionRoomToClients( + @RequestParam( + name = API.PARAM_INSTITUTION_ID, + required = true, + defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId, + @PathVariable(name = API.PARAM_MODEL_ID) final Long examId, + @RequestParam( + name = API.EXAM_API_SEB_CONNECTION_TOKEN, + required = true) final String connectionTokens) { + + checkAccess(institutionId, examId); + + final ProctoringSettings settings = this.examSessionService + .getRunningExam(examId) + .flatMap(this.examAdminService::getExamProctoring) + .getOrThrow(); + + final ExamProctoringService examProctoringService = this.examAdminService + .getExamProctoringService(settings.serverType) + .getOrThrow(); + + Arrays.asList(StringUtils.split(connectionTokens, Constants.LIST_SEPARATOR)) + .stream() + .forEach(connectionToken -> { + examProctoringService + .getClientExamCollectionRoomConnectionData( + settings, + connectionToken) + .flatMap(data -> this.sendJoinInstruction(examId, connectionToken, data)) + .onError(error -> log.error("Failed to send rejoin for: {} cause: {}", + connectionToken, + error.getMessage())); + }); + } + + @RequestMapping( + path = API.MODEL_ID_VAR_PATH_SEGMENT + + API.EXAM_PROCTORING_JOIN_ROOM_PATH_SEGMENT, + method = RequestMethod.POST, + produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public SEBProctoringConnectionData sendJoinProctoringRoomToClients( + @RequestParam( + name = API.PARAM_INSTITUTION_ID, + required = true, + defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId, + @PathVariable(name = API.PARAM_MODEL_ID) final Long examId, + @RequestParam( + name = SEBProctoringConnectionData.ATTR_ROOM_NAME, + required = true) final String roomName, + @RequestParam( + name = SEBProctoringConnectionData.ATTR_SUBJECT, + required = false) final String subject, + @RequestParam( + name = API.EXAM_API_SEB_CONNECTION_TOKEN, + required = true) final String connectionTokens) { + + checkAccess(institutionId, examId); + + final ProctoringSettings settings = this.examSessionService + .getRunningExam(examId) + .flatMap(this.examAdminService::getExamProctoring) + .getOrThrow(); + + final ExamProctoringService examProctoringService = this.examAdminService + .getExamProctoringService(settings.serverType) + .getOrThrow(); + + if (StringUtils.isNotBlank(connectionTokens)) { + final boolean single = connectionTokens.contains(Constants.LIST_SEPARATOR); + (single + ? Arrays.asList(StringUtils.split(connectionTokens, Constants.LIST_SEPARATOR)) + : Arrays.asList(connectionTokens)) + .stream() + .forEach(connectionToken -> { + final SEBProctoringConnectionData data = (single) + ? examProctoringService + .getClientRoomConnectionData(settings, connectionToken) + .onError(error -> log.error( + "Failed to get client room connection data for {} cause: {}", + connectionToken, + error.getMessage())) + .get() + : examProctoringService + .getClientRoomConnectionData( + settings, + connectionToken, + roomName, + (StringUtils.isNotBlank(subject)) ? subject : roomName) + .onError(error -> log.error( + "Failed to get client room connection data for {} cause: {}", + connectionToken, + error.getMessage())) + .get(); + if (data != null) { + sendJoinInstruction(examId, connectionToken, data) + .onError(error -> log.error( + "Failed to send proctoring leave instruction to client: {} cause: {}", + connectionToken, + error.getMessage())); + } + }); + } + + return examProctoringService.createProctorPublicRoomConnection( + settings, + roomName, + (StringUtils.isNotBlank(subject)) ? subject : roomName) + .getOrThrow(); + } + + @RequestMapping( + path = API.MODEL_ID_VAR_PATH_SEGMENT + + API.EXAM_PROCTORING_JON_ALL_COLLECTING_ROOM, + method = RequestMethod.POST, + produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public SEBProctoringConnectionData sendJoinAllCollectingRoomToClients( + @RequestParam( + name = API.PARAM_INSTITUTION_ID, + required = true, + defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId, + @PathVariable(name = API.PARAM_MODEL_ID) final Long examId, + @RequestParam( + name = SEBProctoringConnectionData.ATTR_SUBJECT, + required = false) final String subject) { + + checkAccess(institutionId, examId); + + final ProctoringSettings settings = this.examSessionService + .getRunningExam(examId) + .flatMap(this.examAdminService::getExamProctoring) + .getOrThrow(); + + final ExamProctoringService examProctoringService = this.examAdminService + .getExamProctoringService(settings.serverType) + .getOrThrow(); + + // first create and register a room to collect all connection of the exam + // As long as the room exists new connections will join this room immediately + // after have been applied to the default collecting room + final String roomName = this.examProcotringRoomService.createCollectAllRoom(examId) + .onError(error -> this.examProcotringRoomService.disposeCollectAllRoom(examId)) + .getOrThrow(); + + // get all active connections for the exam and send the join instruction + this.examSessionService.getConnectionData( + examId, + cData -> cData.clientConnection.status == ConnectionStatus.ACTIVE + || cData.clientConnection.status == ConnectionStatus.CLOSED) + .getOrThrow() + .stream() + .forEach(cc -> { + final SEBProctoringConnectionData data = examProctoringService + .getClientRoomConnectionData( + settings, + cc.clientConnection.connectionToken, + roomName, + (StringUtils.isNotBlank(subject)) ? subject : roomName) + .onError(error -> log.error( + "Failed to get client room connection data for {} cause: {}", + cc.clientConnection.connectionToken, + error.getMessage())) + .get(); + if (data != null) { + sendJoinInstruction(examId, cc.clientConnection.connectionToken, data) + .onError(error -> log.error( + "Failed to send proctoring leave instruction to client: {} ", + cc.clientConnection.connectionToken, error)); + } + }); + + return examProctoringService.createProctorPublicRoomConnection( + settings, + roomName, + (StringUtils.isNotBlank(subject)) ? subject : roomName) + .getOrThrow(); + } + + @RequestMapping( + path = API.MODEL_ID_VAR_PATH_SEGMENT + + API.EXAM_PROCTORING_REJON_ALL_COLLECTING_ROOM, + method = RequestMethod.POST, + produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public void sendReJoinAllCollectingRoomToClients( + @RequestParam( + name = API.PARAM_INSTITUTION_ID, + required = true, + defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId, + @PathVariable(name = API.PARAM_MODEL_ID) final Long examId) { + + checkAccess(institutionId, examId); + + final ProctoringSettings settings = this.examSessionService + .getRunningExam(examId) + .flatMap(this.examAdminService::getExamProctoring) + .getOrThrow(); + + final ExamProctoringService examProctoringService = this.examAdminService + .getExamProctoringService(settings.serverType) + .getOrThrow(); + + // first unregister the current room to collect all connection of the exam + this.examProcotringRoomService.disposeCollectAllRoom(examId); + + // get all active connections for the exam and send the join instruction + this.examSessionService.getConnectionData( + examId, + cData -> cData.clientConnection.status == ConnectionStatus.ACTIVE + || cData.clientConnection.status == ConnectionStatus.CLOSED) + .getOrThrow() + .stream() + .forEach(cc -> { + examProctoringService + .getClientExamCollectionRoomConnectionData( + settings, + cc.clientConnection) + .flatMap(data -> this.sendJoinInstruction( + examId, + cc.clientConnection.connectionToken, + data)) + .onError(error -> log.error("Failed to send rejoin for: {} cause: {}", + cc.clientConnection.connectionToken, + error.getMessage())); + }); + } + + private void sendProctoringInstructions( + final Long examId, + final String roomName, + final String connectionTokens, + final Map attributes) { + + if (attributes.isEmpty()) { + log.warn("Missing reconfigure instruction attributes. Skip sending empty instruction to SEB clients"); + return; + } + + if (StringUtils.isNotBlank(connectionTokens)) { + final boolean single = connectionTokens.contains(Constants.LIST_SEPARATOR); + (single + ? Arrays.asList(StringUtils.split(connectionTokens, Constants.LIST_SEPARATOR)) + : Arrays.asList(connectionTokens)) + .stream() + .forEach(connectionToken -> { + this.sebInstructionService.registerInstruction( + examId, + InstructionType.SEB_RECONFIGURE_SETTINGS, + attributes, + connectionToken, + true) + .onError(error -> log.error( + "Failed to register reconfiguring instruction for connection: {}", + connectionToken, + error)); + + }); + } else if (StringUtils.isNotBlank(roomName)) { + this.examProcotringRoomService.getRoomConnections(examId, roomName) + .getOrThrow() + .stream() + .forEach(connection -> { + this.sebInstructionService.registerInstruction( + examId, + InstructionType.SEB_RECONFIGURE_SETTINGS, + attributes, + connection.connectionToken, + true) + .onError(error -> log.error( + "Failed to register reconfiguring instruction for connection: {}", + connection.connectionToken, + error)); + }); + } else { + throw new RuntimeException("API attribute validation error: missing " + + Domain.REMOTE_PROCTORING_ROOM.ATTR_ID + " and/or" + + API.EXAM_API_SEB_CONNECTION_TOKEN + " attribute"); + } + } + + private Map createProctorInstructionAttributes( + final Boolean sendReceiveAudio, + final Boolean sendReceiveVideo, + final Boolean sendAllowChat, + final String flagValue) { + final Map attributes = new HashMap<>(); + if (BooleanUtils.isTrue(sendReceiveAudio)) { + attributes.put( + ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_RECONFIGURE_SETTINGS.JITSI_RECEIVE_AUDIO, + flagValue); + } + if (BooleanUtils.isTrue(sendReceiveVideo)) { + attributes.put( + ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_RECONFIGURE_SETTINGS.JITSI_RECEIVE_VIDEO, + flagValue); + } + if (BooleanUtils.isTrue(sendAllowChat)) { + attributes.put( + ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_RECONFIGURE_SETTINGS.JITSI_ALLOW_CHAT, + flagValue); + } + return attributes; + } + + private Result sendJoinInstruction( + final Long examId, + final String connectionToken, + final SEBProctoringConnectionData data) { + + final Map attributes = new HashMap<>(); + attributes.put( + ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.SERVICE_TYPE, + ProctoringSettings.ProctoringServerType.JITSI_MEET.name()); + attributes.put( + ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.METHOD, + ClientInstruction.ProctoringInstructionMethod.JOIN.name()); + attributes.put( + ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.JITSI_URL, + data.serverURL); + attributes.put( + ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.JITSI_ROOM, + data.roomName); + attributes.put( + ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.JITSI_TOKEN, + data.accessToken); + return this.sebInstructionService.registerInstruction( + examId, + InstructionType.SEB_PROCTORING, + attributes, + connectionToken, + true); + } + + private void checkAccess(final Long institutionId, final Long examId) { + this.authorization.check( + PrivilegeType.READ, + EntityType.EXAM, + institutionId); + + this.authorization.checkRead(this.examSessionService + .getExamDAO() + .byPK(examId) + .getOrThrow()); + } + +} diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index da1f2cec..b9fb7a9c 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -1449,7 +1449,9 @@ sebserver.monitoring.exam.action.list.view=Monitoring sebserver.monitoring.exam.action.viewroom=View {0} ( {1} / {2} ) sebserver.exam.monitoring.action.category.filter=Filter sebserver.exam.overall.action.category.proctoring=Proctoring +sebserver.monitoring.exam.action.proctoring.allRoom=View All +sebserver.monitoring.exam.proctoring.room.all.name=Exam Room sebserver.monitoring.exam.proctoring.action.close=Close Window sebserver.monitoring.exam.proctoring.action.broadcaston.audio=Start Audio Broadcast sebserver.monitoring.exam.proctoring.action.broadcastoff.audio=End Audio Broadcast