SEBSERV-139 added collecting all room feature

This commit is contained in:
anhefti 2020-10-28 15:20:58 +01:00
parent 6e3549dbb7
commit 057275ba49
No known key found for this signature in database
GPG key ID: E9AD9471B6BC114D
24 changed files with 863 additions and 561 deletions

View file

@ -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_RESTRICTION_PATH_SEGMENT = "/check-seb-restriction";
public static final String EXAM_ADMINISTRATION_CHECK_IMPORTED_PATH_SEGMENT = "/check-imported"; 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 EXAM_ADMINISTRATION_SEB_RESTRICTION_CHAPTERS_PATH_SEGMENT = "/chapters";
public static final String PROCTORING_PATH_SEGMENT = "/proctoring"; public static final String EXAM_ADMINISTRATION_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_INDICATOR_ENDPOINT = "/indicator"; 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 = public static final String EXAM_MONITORING_SEB_CONNECTION_TOKEN_PATH_SEGMENT =
"/{" + EXAM_API_SEB_CONNECTION_TOKEN + "}"; "/{" + 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_CONNECTION_ENDPOINT = "/seb-client-connection";
public static final String SEB_CLIENT_EVENT_ENDPOINT = "/seb-client-event"; public static final String SEB_CLIENT_EVENT_ENDPOINT = "/seb-client-event";

View file

@ -372,16 +372,25 @@ public class MonitoringClientConnection implements TemplateComposer {
final String roomName = urlEncoder.encodeToString(Utils.toByteArray(connectionToken)); final String roomName = urlEncoder.encodeToString(Utils.toByteArray(connectionToken));
final String examId = action.getEntityKey().modelId; final String examId = action.getEntityKey().modelId;
final SEBProctoringConnectionData proctoringConnectionData = this.pageService.getCurrentUser() final ProctoringGUIService proctoringGUIService = this.pageService
.getProctoringGUIService() .getCurrentUser()
.getProctoringGUIService();
if (!proctoringGUIService.hasRoom(roomName)) {
final SEBProctoringConnectionData proctoringConnectionData = proctoringGUIService
.registerNewSingleProcotringRoom( .registerNewSingleProcotringRoom(
examId, examId,
roomName, roomName,
connectionData.clientConnection.userSessionId, connectionData.clientConnection.userSessionId,
connectionToken) connectionToken)
.getOrThrow(); .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 JavaScriptExecutor javaScriptExecutor = RWT.getClient().getService(JavaScriptExecutor.class);
final String script = String.format( final String script = String.format(
OPEN_SINGEL_ROOM_SCRIPT, OPEN_SINGEL_ROOM_SCRIPT,
@ -389,9 +398,7 @@ public class MonitoringClientConnection implements TemplateComposer {
this.guiServiceInfo.getExternalServerURIBuilder().toUriString(), this.guiServiceInfo.getExternalServerURIBuilder().toUriString(),
this.remoteProctoringEndpoint); this.remoteProctoringEndpoint);
javaScriptExecutor.execute(script); javaScriptExecutor.execute(script);
this.pageService.getCurrentUser() proctoringGUIService.registerProctoringWindow(roomName);
.getProctoringGUIService()
.registerProctoringWindow(roomName);
return action; return action;
} }

View file

@ -108,6 +108,8 @@ public class MonitoringRunningExam implements TemplateComposer {
new LocTextKey("sebserver.monitoring.exam.connection.action.instruction.quit.all.confirm"); new LocTextKey("sebserver.monitoring.exam.connection.action.instruction.quit.all.confirm");
private static final LocTextKey CONFIRM_DISABLE_SELECTED = private static final LocTextKey CONFIRM_DISABLE_SELECTED =
new LocTextKey("sebserver.monitoring.exam.connection.action.instruction.disable.selected.confirm"); 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 ServerPushService serverPushService;
private final PageService pageService; private final PageService pageService;
@ -329,6 +331,13 @@ public class MonitoringRunningExam implements TemplateComposer {
.getOr(null); .getOr(null);
if (proctoringSettings != null && proctoringSettings.enableProctoring) { if (proctoringSettings != null && proctoringSettings.enableProctoring) {
actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_CREATE_ALL_PROCTOR_ROOM)
.withEntityKey(entityKey)
.withExec(this::createCollectingAllRoom)
.noEventPropagation()
.publish();
final Map<String, Pair<RemoteProctoringRoom, TreeItem>> availableRooms = new HashMap<>(); final Map<String, Pair<RemoteProctoringRoom, TreeItem>> availableRooms = new HashMap<>();
updateRoomActions( updateRoomActions(
entityKey, 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( private void updateRoomActions(
final EntityKey entityKey, final EntityKey entityKey,
final PageContext pageContext, final PageContext pageContext,
@ -359,7 +403,8 @@ public class MonitoringRunningExam implements TemplateComposer {
this.pageService.getRestService().getBuilder(GetProcotringRooms.class) this.pageService.getRestService().getBuilder(GetProcotringRooms.class)
.withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId) .withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId)
.call() .call()
.getOrThrow() .onError(error -> log.error("Failed to update proctoring rooms on GUI {}", error.getMessage()))
.getOr(Collections.emptyList())
.stream() .stream()
.forEach(room -> { .forEach(room -> {
if (rooms.containsKey(room.name)) { if (rooms.containsKey(room.name)) {

View file

@ -704,6 +704,11 @@ public enum ActionDefinition {
ImageIcon.PROCTOR_ROOM, ImageIcon.PROCTOR_ROOM,
PageStateDefinitionImpl.MONITORING_RUNNING_EXAM, PageStateDefinitionImpl.MONITORING_RUNNING_EXAM,
ActionCategory.PROCTORING), 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( LOGS_USER_ACTIVITY_LIST(
new LocTextKey("sebserver.logs.activity.userlogs"), new LocTextKey("sebserver.logs.activity.userlogs"),

View file

@ -36,7 +36,7 @@ public class GetProctoringSettings extends RestCall<ProctoringSettings> {
MediaType.APPLICATION_JSON_UTF8, MediaType.APPLICATION_JSON_UTF8,
API.EXAM_ADMINISTRATION_ENDPOINT API.EXAM_ADMINISTRATION_ENDPOINT
+ API.MODEL_ID_VAR_PATH_SEGMENT + API.MODEL_ID_VAR_PATH_SEGMENT
+ API.PROCTORING_PATH_SEGMENT); + API.EXAM_ADMINISTRATION_PROCTORING_PATH_SEGMENT);
} }
} }

View file

@ -36,7 +36,7 @@ public class SaveProctoringSettings extends RestCall<Exam> {
MediaType.APPLICATION_JSON_UTF8, MediaType.APPLICATION_JSON_UTF8,
API.EXAM_ADMINISTRATION_ENDPOINT API.EXAM_ADMINISTRATION_ENDPOINT
+ API.MODEL_ID_VAR_PATH_SEGMENT + API.MODEL_ID_VAR_PATH_SEGMENT
+ API.PROCTORING_PATH_SEGMENT); + API.EXAM_ADMINISTRATION_PROCTORING_PATH_SEGMENT);
} }
} }

View file

@ -8,8 +8,6 @@
package ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session; package ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session;
import java.util.List;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
@ -26,19 +24,19 @@ import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
@Lazy @Lazy
@Component @Component
@GuiProfile @GuiProfile
public class LeaveRemoteProctoringRoom extends RestCall<List<SEBProctoringConnectionData>> { public class CreateCollectingAllProctoringRoom extends RestCall<SEBProctoringConnectionData> {
public LeaveRemoteProctoringRoom() { public CreateCollectingAllProctoringRoom() {
super(new TypeKey<>( super(new TypeKey<>(
CallType.UNDEFINED, CallType.UNDEFINED,
EntityType.EXAM_PROCTOR_DATA, EntityType.EXAM_PROCTOR_DATA,
new TypeReference<List<SEBProctoringConnectionData>>() { new TypeReference<SEBProctoringConnectionData>() {
}), }),
HttpMethod.POST, HttpMethod.POST,
MediaType.APPLICATION_FORM_URLENCODED, MediaType.APPLICATION_FORM_URLENCODED,
API.EXAM_MONITORING_ENDPOINT API.EXAM_PROCTORING_ENDPOINT
+ API.MODEL_ID_VAR_PATH_SEGMENT + API.MODEL_ID_VAR_PATH_SEGMENT
+ API.PROCTORING_PATH_SEGMENT + API.EXAM_PROCTORING_JON_ALL_COLLECTING_ROOM);
+ API.PROCTORING_LEAVE_ROOM_PATH_SEGMENT);
} }
} }

View file

@ -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<Void> {
public DisposeCollectingAllProctoringRoom() {
super(new TypeKey<>(
CallType.UNDEFINED,
EntityType.EXAM_PROCTOR_DATA,
new TypeReference<Void>() {
}),
HttpMethod.POST,
MediaType.APPLICATION_FORM_URLENCODED,
API.EXAM_PROCTORING_ENDPOINT
+ API.MODEL_ID_VAR_PATH_SEGMENT
+ API.EXAM_PROCTORING_REJON_ALL_COLLECTING_ROOM);
}
}

View file

@ -36,9 +36,8 @@ public class GetProcotringRooms extends RestCall<Collection<RemoteProctoringRoom
}), }),
HttpMethod.GET, HttpMethod.GET,
MediaType.APPLICATION_FORM_URLENCODED, MediaType.APPLICATION_FORM_URLENCODED,
API.EXAM_MONITORING_ENDPOINT API.EXAM_PROCTORING_ENDPOINT
+ API.MODEL_ID_VAR_PATH_SEGMENT + API.MODEL_ID_VAR_PATH_SEGMENT
+ API.PROCTORING_PATH_SEGMENT + API.EXAM_PROCTORING_ROOMS_SEGMENT);
+ API.PROCTORING_ROOMS_SEGMENT);
} }
} }

View file

@ -34,9 +34,8 @@ public class GetProctorRoomConnectionData extends RestCall<SEBProctoringConnecti
}), }),
HttpMethod.GET, HttpMethod.GET,
MediaType.APPLICATION_FORM_URLENCODED, MediaType.APPLICATION_FORM_URLENCODED,
API.EXAM_MONITORING_ENDPOINT API.EXAM_PROCTORING_ENDPOINT
+ API.MODEL_ID_VAR_PATH_SEGMENT + API.MODEL_ID_VAR_PATH_SEGMENT);
+ API.PROCTORING_PATH_SEGMENT);
} }
} }

View file

@ -36,10 +36,9 @@ public class GetProctorRoomConnections extends RestCall<Collection<ClientConnect
}), }),
HttpMethod.GET, HttpMethod.GET,
MediaType.APPLICATION_FORM_URLENCODED, MediaType.APPLICATION_FORM_URLENCODED,
API.EXAM_MONITORING_ENDPOINT API.EXAM_PROCTORING_ENDPOINT
+ API.MODEL_ID_VAR_PATH_SEGMENT + API.MODEL_ID_VAR_PATH_SEGMENT
+ API.PROCTORING_PATH_SEGMENT + API.EXAM_PROCTORING_ROOM_CONNECTIONS_PATH_SEGMENT);
+ API.PROCTORING_ROOM_CONNECTIONS_PATH_SEGMENT);
} }
} }

View file

@ -34,10 +34,9 @@ public class SendJoinRemoteProctoringRoom extends RestCall<SEBProctoringConnecti
}), }),
HttpMethod.POST, HttpMethod.POST,
MediaType.APPLICATION_FORM_URLENCODED, MediaType.APPLICATION_FORM_URLENCODED,
API.EXAM_MONITORING_ENDPOINT API.EXAM_PROCTORING_ENDPOINT
+ API.MODEL_ID_VAR_PATH_SEGMENT + API.MODEL_ID_VAR_PATH_SEGMENT
+ API.PROCTORING_PATH_SEGMENT + API.EXAM_PROCTORING_JOIN_ROOM_PATH_SEGMENT);
+ API.PROCTORING_JOIN_ROOM_PATH_SEGMENT);
} }
} }

View file

@ -33,10 +33,9 @@ public class SendProctoringBroadcastOffInstruction extends RestCall<Void> {
}), }),
HttpMethod.POST, HttpMethod.POST,
MediaType.APPLICATION_FORM_URLENCODED, MediaType.APPLICATION_FORM_URLENCODED,
API.EXAM_MONITORING_ENDPOINT API.EXAM_PROCTORING_ENDPOINT
+ API.MODEL_ID_VAR_PATH_SEGMENT + API.MODEL_ID_VAR_PATH_SEGMENT
+ API.PROCTORING_PATH_SEGMENT + API.EXAM_PROCTORING_BROADCAST_OFF_PATH_SEGMENT);
+ API.PROCTORING_BROADCAST_OFF_PATH_SEGMENT);
} }
} }

View file

@ -33,10 +33,9 @@ public class SendProctoringBroadcastOnInstruction extends RestCall<Void> {
}), }),
HttpMethod.POST, HttpMethod.POST,
MediaType.APPLICATION_FORM_URLENCODED, MediaType.APPLICATION_FORM_URLENCODED,
API.EXAM_MONITORING_ENDPOINT API.EXAM_PROCTORING_ENDPOINT
+ API.MODEL_ID_VAR_PATH_SEGMENT + API.MODEL_ID_VAR_PATH_SEGMENT
+ API.PROCTORING_PATH_SEGMENT + API.EXAM_PROCTORING_BROADCAST_ON_PATH_SEGMENT);
+ API.PROCTORING_BROADCAST_ON_PATH_SEGMENT);
} }
} }

View file

@ -33,10 +33,9 @@ public class SendRejoinExamCollectionRoom extends RestCall<Void> {
}), }),
HttpMethod.POST, HttpMethod.POST,
MediaType.APPLICATION_FORM_URLENCODED, MediaType.APPLICATION_FORM_URLENCODED,
API.EXAM_MONITORING_ENDPOINT API.EXAM_PROCTORING_ENDPOINT
+ API.MODEL_ID_VAR_PATH_SEGMENT + API.MODEL_ID_VAR_PATH_SEGMENT
+ API.PROCTORING_PATH_SEGMENT + API.EXAM_PROCTORING_REJOIN_COLLECTING_ROOM_PATH_SEGMENT);
+ API.PROCTORING_REJOIN_EXAM_ROOM_PATH_SEGMENT);
} }
} }

View file

@ -14,6 +14,7 @@ import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger; 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.model.exam.SEBProctoringConnectionData;
import ch.ethz.seb.sebserver.gbl.util.Result; 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.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.SendJoinRemoteProctoringRoom;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.SendRejoinExamCollectionRoom; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.SendRejoinExamCollectionRoom;
@ -84,37 +87,63 @@ public class ProctoringGUIService {
new ProctoringWindowData(examId, data)); 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<SEBProctoringConnectionData> registerNewSingleProcotringRoom( public Result<SEBProctoringConnectionData> registerNewSingleProcotringRoom(
final String examId, final String examId,
final String roomName, final String roomName,
final String subject, final String subject,
final String connectionToken) { final String connectionToken) {
return Result.tryCatch(() -> { return this.restService.getBuilder(SendJoinRemoteProctoringRoom.class)
final SEBProctoringConnectionData connection =
this.restService.getBuilder(SendJoinRemoteProctoringRoom.class)
.withURIVariable(API.PARAM_MODEL_ID, examId) .withURIVariable(API.PARAM_MODEL_ID, examId)
.withFormParam(SEBProctoringConnectionData.ATTR_ROOM_NAME, roomName) .withFormParam(SEBProctoringConnectionData.ATTR_ROOM_NAME, roomName)
.withFormParam(SEBProctoringConnectionData.ATTR_SUBJECT, subject) .withFormParam(SEBProctoringConnectionData.ATTR_SUBJECT, subject)
.withFormParam(API.EXAM_API_SEB_CONNECTION_TOKEN, connectionToken) .withFormParam(API.EXAM_API_SEB_CONNECTION_TOKEN, connectionToken)
.call() .call()
.getOrThrow(); .map(connection -> {
this.rooms.put(roomName, new RoomConnectionData(roomName, examId, connectionToken)); this.rooms.put(roomName, new RoomConnectionData(roomName, examId, connectionToken));
this.openWindows.add(roomName); this.openWindows.add(roomName);
return connection; return connection;
}); });
} }
public Result<SEBProctoringConnectionData> 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<SEBProctoringConnectionData> registerNewProcotringRoom( public Result<SEBProctoringConnectionData> registerNewProcotringRoom(
final String examId, final String examId,
final String roomName, final String roomName,
final String subject, final String subject,
final Collection<String> connectionTokens) { final Collection<String> connectionTokens) {
return Result.tryCatch(() -> { return this.restService.getBuilder(SendJoinRemoteProctoringRoom.class)
final SEBProctoringConnectionData connection =
this.restService.getBuilder(SendJoinRemoteProctoringRoom.class)
.withURIVariable(API.PARAM_MODEL_ID, examId) .withURIVariable(API.PARAM_MODEL_ID, examId)
.withFormParam(SEBProctoringConnectionData.ATTR_ROOM_NAME, roomName) .withFormParam(SEBProctoringConnectionData.ATTR_ROOM_NAME, roomName)
.withFormParam(SEBProctoringConnectionData.ATTR_SUBJECT, subject) .withFormParam(SEBProctoringConnectionData.ATTR_SUBJECT, subject)
@ -122,8 +151,7 @@ public class ProctoringGUIService {
API.EXAM_API_SEB_CONNECTION_TOKEN, API.EXAM_API_SEB_CONNECTION_TOKEN,
StringUtils.join(connectionTokens, Constants.LIST_SEPARATOR_CHAR)) StringUtils.join(connectionTokens, Constants.LIST_SEPARATOR_CHAR))
.call() .call()
.getOrThrow(); .map(connection -> {
this.rooms.put(roomName, new RoomConnectionData(roomName, examId, connectionTokens)); this.rooms.put(roomName, new RoomConnectionData(roomName, examId, connectionTokens));
this.openWindows.add(roomName); this.openWindows.add(roomName);
return connection; return connection;
@ -147,7 +175,9 @@ public class ProctoringGUIService {
API.EXAM_API_SEB_CONNECTION_TOKEN, API.EXAM_API_SEB_CONNECTION_TOKEN,
StringUtils.join(connectionTokens, Constants.LIST_SEPARATOR_CHAR)) StringUtils.join(connectionTokens, Constants.LIST_SEPARATOR_CHAR))
.call() .call()
.getOrThrow(); .onError(error -> log.error("Failed to add connection to proctoring room: {} {}",
room,
error.getMessage()));
roomConnectionData.connections.addAll(connectionTokens); roomConnectionData.connections.addAll(connectionTokens);
} }
} }
@ -164,15 +194,24 @@ public class ProctoringGUIService {
final RoomConnectionData roomConnectionData = this.rooms.remove(name); final RoomConnectionData roomConnectionData = this.rooms.remove(name);
if (roomConnectionData != null) { if (roomConnectionData != null) {
// send instruction to leave this room and join the own exam collection room // send instruction to leave this room and join the own exam collection room
if (!roomConnectionData.connections.isEmpty()) {
this.restService.getBuilder(SendRejoinExamCollectionRoom.class) this.restService.getBuilder(SendRejoinExamCollectionRoom.class)
.withURIVariable(API.PARAM_MODEL_ID, roomConnectionData.examId) .withURIVariable(API.PARAM_MODEL_ID, roomConnectionData.examId)
.withFormParam( .withFormParam(
API.EXAM_API_SEB_CONNECTION_TOKEN, API.EXAM_API_SEB_CONNECTION_TOKEN,
StringUtils.join(roomConnectionData.connections, Constants.LIST_SEPARATOR_CHAR)) StringUtils.join(roomConnectionData.connections, Constants.LIST_SEPARATOR_CHAR))
.call() .call()
.getOrThrow(); .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()));
}
} }
} }

View file

@ -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;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings.ProctoringServerType; 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.exam.SEBProctoringConnectionData;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
public interface ExamProctoringService { public interface ExamProctoringService {
@ -20,14 +21,18 @@ public interface ExamProctoringService {
Result<Boolean> testExamProctoring(final ProctoringSettings examProctoring); Result<Boolean> testExamProctoring(final ProctoringSettings examProctoring);
Result<SEBProctoringConnectionData> createProctorPublicRoomConnection( Result<SEBProctoringConnectionData> createProctorPublicRoomConnection(
final ProctoringSettings examProctoring, final ProctoringSettings proctoringSettings,
final String roomName, final String roomName,
final String subject); final String subject);
Result<SEBProctoringConnectionData> getClientExamCollectionRoomConnectionData( Result<SEBProctoringConnectionData> getClientExamCollectionRoomConnectionData(
final ProctoringSettings examProctoring, final ProctoringSettings proctoringSettings,
final String connectionToken); final String connectionToken);
Result<SEBProctoringConnectionData> getClientExamCollectionRoomConnectionData(
final ProctoringSettings proctoringSettings,
final ClientConnection connection);
Result<SEBProctoringConnectionData> getClientExamCollectionRoomConnectionData( Result<SEBProctoringConnectionData> getClientExamCollectionRoomConnectionData(
final ProctoringSettings proctoringSettings, final ProctoringSettings proctoringSettings,
final String connectionToken, final String connectionToken,

View file

@ -13,6 +13,7 @@ import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.util.Base64; import java.util.Base64;
import java.util.Base64.Encoder; import java.util.Base64.Encoder;
import java.util.Collection;
import javax.crypto.Mac; import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec; 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;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings.ProctoringServerType; 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.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.ClientConnectionData;
import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom; import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
@ -99,15 +101,27 @@ public class ExamJITSIProctoringService implements ExamProctoringService {
final ProctoringSettings proctoringSettings, final ProctoringSettings proctoringSettings,
final String connectionToken) { final String connectionToken) {
return Result.tryCatch(() -> { return this.examSessionService
final ClientConnectionData clientConnection = this.examSessionService
.getConnectionData(connectionToken) .getConnectionData(connectionToken)
.getOrThrow(); .flatMap(connection -> getClientExamCollectionRoomConnectionData(
final RemoteProctoringRoom room = this.examSessionService proctoringSettings,
connection.clientConnection));
}
@Override
public Result<SEBProctoringConnectionData> getClientExamCollectionRoomConnectionData(
final ProctoringSettings proctoringSettings,
final ClientConnection connection) {
return Result.tryCatch(() -> {
final Collection<RemoteProctoringRoom> remoteProctoringRooms = this.examSessionService
.getExamSessionCacheService() .getExamSessionCacheService()
.getRemoteProctoringRooms(clientConnection.clientConnection.examId) .getRemoteProctoringRooms(connection.examId);
final RemoteProctoringRoom room = remoteProctoringRooms
.stream() .stream()
.filter(r -> r.id.equals(clientConnection.clientConnection.remoteProctoringRoomId)) .filter(r -> r.id.equals(connection.remoteProctoringRoomId))
.findFirst() .findFirst()
.orElseGet(() -> { .orElseGet(() -> {
throw new RuntimeException("No exam proctoring room found for clientConnection"); throw new RuntimeException("No exam proctoring room found for clientConnection");
@ -119,10 +133,10 @@ public class ExamJITSIProctoringService implements ExamProctoringService {
proctoringSettings.serverURL, proctoringSettings.serverURL,
proctoringSettings.appKey, proctoringSettings.appKey,
proctoringSettings.getAppSecret(), proctoringSettings.getAppSecret(),
clientConnection.clientConnection.userSessionId, connection.userSessionId,
"seb-client", "seb-client",
room.name, room.name,
clientConnection.clientConnection.userSessionId, connection.userSessionId,
forExam(proctoringSettings)) forExam(proctoringSettings))
.getOrThrow(); .getOrThrow();
}); });

View file

@ -124,7 +124,8 @@ public class ExamProcotringRoomServiceImpl implements ExamProcotringRoomService
.build() .build()
.execute(); .execute();
flagUpdated(toUpdate).stream() flagUpdated(toUpdate)
.stream()
.forEach(cc -> { .forEach(cc -> {
if (ConnectionStatus.ACTIVE.name().equals(cc.getStatus())) { if (ConnectionStatus.ACTIVE.name().equals(cc.getStatus())) {
assignToRoom(cc); assignToRoom(cc);
@ -156,13 +157,14 @@ public class ExamProcotringRoomServiceImpl implements ExamProcotringRoomService
ACTIVE_COLLECTING_ALL_ROOM_ATTRIBUTE_NAME); ACTIVE_COLLECTING_ALL_ROOM_ATTRIBUTE_NAME);
} }
private Result<String> getActiveCollectingAllRoom(final Long examId) { private String getActiveCollectingAllRoom(final Long examId) {
return this.additionalAttributesDAO return this.additionalAttributesDAO
.getAdditionalAttribute( .getAdditionalAttribute(
EntityType.EXAM, EntityType.EXAM,
examId, examId,
ACTIVE_COLLECTING_ALL_ROOM_ATTRIBUTE_NAME) ACTIVE_COLLECTING_ALL_ROOM_ATTRIBUTE_NAME)
.map(attr -> attr.getValue()); .map(attr -> attr.getValue())
.getOr(null);
} }
// TODO considering doing bulk update here // TODO considering doing bulk update here
@ -202,8 +204,14 @@ public class ExamProcotringRoomServiceImpl implements ExamProcotringRoomService
proctoringRoom.id, proctoringRoom.id,
0)); 0));
this.examSessionCacheService.evictRemoteProctoringRooms(cc.getExamId()); this.examSessionCacheService.evictRemoteProctoringRooms(cc.getExamId());
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); applyProcotringInstruction(cc.getExamId(), cc.getConnectionToken(), proctoringRoom.name);
} }
}
} catch (final Exception e) { } catch (final Exception e) {
log.error("Failed to process proctoring room update for client connection: {}", cc.getConnectionToken(), e); log.error("Failed to process proctoring room update for client connection: {}", cc.getConnectionToken(), e);
try { try {

View file

@ -21,7 +21,6 @@ import java.util.stream.Collectors;
import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.BooleanUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager; import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.AccessDeniedException;
@ -314,9 +313,15 @@ public class ExamSessionServiceImpl implements ExamSessionService {
@Override @Override
public Result<ClientConnectionData> getConnectionData(final String connectionToken) { public Result<ClientConnectionData> getConnectionData(final String connectionToken) {
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
final Cache cache = this.cacheManager.getCache(ExamSessionCacheService.CACHE_NAME_ACTIVE_CLIENT_CONNECTION); final ClientConnectionDataInternal activeClientConnection = this.examSessionCacheService
return cache.get(connectionToken, ClientConnectionData.class); .getActiveClientConnection(connectionToken);
if (activeClientConnection == null) {
throw new NoSuchElementException("Client Connection with token: " + connectionToken);
}
return activeClientConnection;
}); });
} }

View file

@ -383,7 +383,7 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
@RequestMapping( @RequestMapping(
path = API.MODEL_ID_VAR_PATH_SEGMENT path = API.MODEL_ID_VAR_PATH_SEGMENT
+ API.PROCTORING_PATH_SEGMENT, + API.EXAM_ADMINISTRATION_PROCTORING_PATH_SEGMENT,
method = RequestMethod.GET, method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_UTF8_VALUE) produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public ProctoringSettings getExamProctoring( public ProctoringSettings getExamProctoring(
@ -402,7 +402,7 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
@RequestMapping( @RequestMapping(
path = API.MODEL_ID_VAR_PATH_SEGMENT path = API.MODEL_ID_VAR_PATH_SEGMENT
+ API.PROCTORING_PATH_SEGMENT, + API.EXAM_ADMINISTRATION_PROCTORING_PATH_SEGMENT,
method = RequestMethod.POST, method = RequestMethod.POST,
produces = MediaType.APPLICATION_JSON_UTF8_VALUE) produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public Exam saveExamProctoring( public Exam saveExamProctoring(

View file

@ -9,19 +9,14 @@
package ch.ethz.seb.sebserver.webservice.weblayer.api; package ch.ethz.seb.sebserver.webservice.weblayer.api;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid; import javax.validation.Valid;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; 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.Domain;
import ch.ethz.seb.sebserver.gbl.model.Page; 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.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.ClientConnection.ConnectionStatus;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData; 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;
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.model.user.UserRole;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; 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.PaginationService;
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.AuthorizationService; 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.PermissionDeniedException;
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.UserService; 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.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.ExamSessionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientConnectionService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientConnectionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBInstructionService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBInstructionService;
@ -76,27 +62,21 @@ public class ExamMonitoringController {
private final SEBClientConnectionService sebClientConnectionService; private final SEBClientConnectionService sebClientConnectionService;
private final ExamSessionService examSessionService; private final ExamSessionService examSessionService;
private final ExamAdminService examAdminService;
private final SEBInstructionService sebInstructionService; private final SEBInstructionService sebInstructionService;
private final AuthorizationService authorization; private final AuthorizationService authorization;
private final PaginationService paginationService; private final PaginationService paginationService;
private final ExamProcotringRoomService examProcotringRoomService;
public ExamMonitoringController( public ExamMonitoringController(
final ExamAdminService examAdminService,
final SEBClientConnectionService sebClientConnectionService, final SEBClientConnectionService sebClientConnectionService,
final SEBInstructionService sebInstructionService, final SEBInstructionService sebInstructionService,
final AuthorizationService authorization, final AuthorizationService authorization,
final PaginationService paginationService, final PaginationService paginationService) {
final ExamProcotringRoomService examProcotringRoomService) {
this.examAdminService = examAdminService;
this.sebClientConnectionService = sebClientConnectionService; this.sebClientConnectionService = sebClientConnectionService;
this.examSessionService = sebClientConnectionService.getExamSessionService(); this.examSessionService = sebClientConnectionService.getExamSessionService();
this.sebInstructionService = sebInstructionService; this.sebInstructionService = sebInstructionService;
this.authorization = authorization; this.authorization = authorization;
this.paginationService = paginationService; this.paginationService = paginationService;
this.examProcotringRoomService = examProcotringRoomService;
} }
/** This is called by Spring to initialize the WebDataBinder and is used here to /** 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<RemoteProctoringRoom> 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<ClientConnection> 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<String, String> 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<String, String> 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<Void> 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<String, String> 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<String, String> createProctorInstructionAttributes(
final Boolean sendReceiveAudio,
final Boolean sendReceiveVideo,
final Boolean sendAllowChat,
final String flagValue) {
final Map<String, String> 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) { private boolean hasRunningExamPrivilege(final Long examId, final Long institution) {
return hasRunningExamPrivilege( return hasRunningExamPrivilege(
this.examSessionService.getRunningExam(examId).getOr(null), this.examSessionService.getRunningExam(examId).getOr(null),
@ -685,53 +284,4 @@ public class ExamMonitoringController {
return exam.institutionId.equals(institution) && exam.isOwner(userId); return exam.institutionId.equals(institution) && exam.isOwner(userId);
} }
private Result<Void> sendJoinInstruction(
final Long examId,
final String connectionToken,
final SEBProctoringConnectionData data) {
final Map<String, String> 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<Void> sendLeaveInstruction(
// final Long examId,
// final String connectionToken,
// final SEBProctoringConnectionData data) {
//
// return sendProctorInstruction(
// examId,
// connectionToken,
// data,
// ClientInstruction.ProctoringInstructionMethod.LEAVE.name());
// }
// PRIVATE RESULT<VOID> SENDPROCTORINSTRUCTION(
// FINAL LONG EXAMID,
// FINAL STRING CONNECTIONTOKEN,
// FINAL SEBPROCTORINGCONNECTIONDATA DATA,
// FINAL STRING METHOD) {
//
// }
} }

View file

@ -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<RemoteProctoringRoom> 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<ClientConnection> 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<String, String> 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<String, String> 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<String, String> 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<String, String> createProctorInstructionAttributes(
final Boolean sendReceiveAudio,
final Boolean sendReceiveVideo,
final Boolean sendAllowChat,
final String flagValue) {
final Map<String, String> 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<Void> sendJoinInstruction(
final Long examId,
final String connectionToken,
final SEBProctoringConnectionData data) {
final Map<String, String> 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());
}
}

View file

@ -1449,7 +1449,9 @@ sebserver.monitoring.exam.action.list.view=Monitoring
sebserver.monitoring.exam.action.viewroom=View {0} ( {1} / {2} ) sebserver.monitoring.exam.action.viewroom=View {0} ( {1} / {2} )
sebserver.exam.monitoring.action.category.filter=Filter sebserver.exam.monitoring.action.category.filter=Filter
sebserver.exam.overall.action.category.proctoring=Proctoring 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.close=Close Window
sebserver.monitoring.exam.proctoring.action.broadcaston.audio=Start Audio Broadcast sebserver.monitoring.exam.proctoring.action.broadcaston.audio=Start Audio Broadcast
sebserver.monitoring.exam.proctoring.action.broadcastoff.audio=End Audio Broadcast sebserver.monitoring.exam.proctoring.action.broadcastoff.audio=End Audio Broadcast