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 f5d1f9db..d3173e70 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 @@ -77,7 +77,7 @@ public class MonitoringClientConnection implements TemplateComposer { // @formatter:off private static final String OPEN_SINGEL_ROOM_SCRIPT = - "var existingWin = window.open('', '%s', 'height=420,width=620,location=no,scrollbars=yes,status=no,menubar=yes,toolbar=yes,titlebar=yes,dialog=yes');\n" + + "var existingWin = window.open('', '%s', 'height=420,width=640,location=no,scrollbars=yes,status=no,menubar=yes,toolbar=yes,titlebar=yes,dialog=yes');\n" + "if(existingWin.location.href === 'about:blank'){\n" + " existingWin.location.href = '%s%s';\n" + " existingWin.focus();\n" + 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 02fbddb4..74208d04 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 @@ -49,6 +49,7 @@ 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.user.UserRole; import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; +import ch.ethz.seb.sebserver.gbl.util.Pair; import ch.ethz.seb.sebserver.gbl.util.Tuple; import ch.ethz.seb.sebserver.gbl.util.Utils; import ch.ethz.seb.sebserver.gui.GuiServiceInfo; @@ -114,6 +115,7 @@ public class MonitoringRunningExam implements TemplateComposer { private final InstructionProcessor instructionProcessor; private final GuiServiceInfo guiServiceInfo; private final long pollInterval; + private final long proctoringRoomUpdateInterval; private final String remoteProctoringEndpoint; private final ProctorRoomConnectionsPopup proctorRoomConnectionsPopup; @@ -124,7 +126,8 @@ public class MonitoringRunningExam implements TemplateComposer { final GuiServiceInfo guiServiceInfo, final ProctorRoomConnectionsPopup proctorRoomConnectionsPopup, @Value("${sebserver.gui.webservice.poll-interval:1000}") final long pollInterval, - @Value("${sebserver.gui.remote.proctoring.entrypoint:/remote-proctoring}") final String remoteProctoringEndpoint) { + @Value("${sebserver.gui.remote.proctoring.entrypoint:/remote-proctoring}") final String remoteProctoringEndpoint, + @Value("${sebserver.gui.remote.proctoring.rooms.update.poll-interval:5000}") final long proctoringRoomUpdateInterval) { this.serverPushService = serverPushService; this.pageService = pageService; @@ -134,6 +137,7 @@ public class MonitoringRunningExam implements TemplateComposer { this.pollInterval = pollInterval; this.remoteProctoringEndpoint = remoteProctoringEndpoint; this.proctorRoomConnectionsPopup = proctorRoomConnectionsPopup; + this.proctoringRoomUpdateInterval = proctoringRoomUpdateInterval; } @Override @@ -325,7 +329,7 @@ public class MonitoringRunningExam implements TemplateComposer { .getOr(null); if (proctoringSettings != null && proctoringSettings.enableProctoring) { - final Map availableRooms = new HashMap<>(); + final Map> availableRooms = new HashMap<>(); updateRoomActions( entityKey, pageContext, @@ -334,7 +338,7 @@ public class MonitoringRunningExam implements TemplateComposer { proctoringSettings); this.serverPushService.runServerPush( new ServerPushContext(content, Utils.truePredicate()), - 5000, + this.proctoringRoomUpdateInterval, context -> updateRoomActions( entityKey, pageContext, @@ -347,7 +351,7 @@ public class MonitoringRunningExam implements TemplateComposer { private void updateRoomActions( final EntityKey entityKey, final PageContext pageContext, - final Map rooms, + final Map> rooms, final PageActionBuilder actionBuilder, final ProctoringSettings proctoringSettings) { @@ -358,9 +362,10 @@ public class MonitoringRunningExam implements TemplateComposer { .getOrThrow() .stream() .forEach(room -> { - if (rooms.containsKey(room)) { + if (rooms.containsKey(room.name)) { // update action - final TreeItem treeItem = rooms.get(room); + final TreeItem treeItem = rooms.get(room.name).b; + rooms.put(room.name, new Pair<>(room, treeItem)); treeItem.setText(i18nSupport.getText(new LocTextKey( ActionDefinition.MONITOR_EXAM_VIEW_PROCTOR_ROOM.title.name, room.subject, @@ -372,7 +377,13 @@ public class MonitoringRunningExam implements TemplateComposer { final PageAction action = actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_VIEW_PROCTOR_ROOM) .withEntityKey(entityKey) - .withExec(a -> showExamProctoringRoom(proctoringSettings, room, rooms, a)) + .withExec(_action -> { + final int actualRoomSize = getActualRoomSize(room, rooms); + if (actualRoomSize <= 0) { + return _action; + } + return showExamProctoringRoom(proctoringSettings, room, _action); + }) .withNameAttributes( room.subject, room.roomSize, @@ -380,15 +391,11 @@ public class MonitoringRunningExam implements TemplateComposer { .noEventPropagation() .create(); - this.pageService.publishAction(action, treeItem -> rooms.put(room, treeItem)); + this.pageService.publishAction( + action, + _treeItem -> rooms.put(room.name, new Pair<>(room, _treeItem))); addRoomConnectionsPopupListener(entityKey, pageContext, rooms); - rooms.entrySet().stream() - .filter(entry -> entry.getKey().equals(room)) - .findFirst() - .ifPresent(entry -> processProctorRoomActionActivation( - entry.getValue(), - room, - pageContext)); + processProctorRoomActionActivation(rooms.get(room.name).b, room, pageContext); } }); } @@ -414,10 +421,10 @@ public class MonitoringRunningExam implements TemplateComposer { private void addRoomConnectionsPopupListener( final EntityKey entityKey, final PageContext pageContext, - final Map rooms) { + final Map> rooms) { if (!rooms.isEmpty()) { - final TreeItem treeItem = rooms.values().iterator().next(); + final TreeItem treeItem = rooms.values().iterator().next().b; final Tree tree = treeItem.getParent(); if (tree.getData(SHOW_CONNECTION_ACTION_APPLIED) == null) { tree.addListener(SWT.Selection, event -> { @@ -426,16 +433,17 @@ public class MonitoringRunningExam implements TemplateComposer { if (event.button == 3) { rooms.entrySet() .stream() - .filter(e -> e.getValue().equals(item)) + .filter(e -> e.getValue().b.equals(item)) .findFirst() .ifPresent(e -> { - if (e.getKey().roomSize > 0) { + final RemoteProctoringRoom room = e.getValue().a; + if (room.roomSize > 0) { final PageContext pc = pageContext.copy() .clearAttributes() - .withEntityKey(new EntityKey(e.getKey().getName(), + .withEntityKey(new EntityKey(room.name, EntityType.REMOTE_PROCTORING_ROOM)) .withParentEntityKey(entityKey); - this.proctorRoomConnectionsPopup.show(pc, e.getKey().getSubject()); + this.proctorRoomConnectionsPopup.show(pc, room.subject); } }); } @@ -445,25 +453,18 @@ public class MonitoringRunningExam implements TemplateComposer { } } - private int getActualRoomSize(final RemoteProctoringRoom room, final Map rooms) { - return rooms.entrySet().stream() - .filter(entry -> entry.getKey().equals(room)) - .findFirst() - .map(entry -> entry.getKey().roomSize) - .orElseGet(() -> 1); + private int getActualRoomSize( + final RemoteProctoringRoom room, + final Map> rooms) { + + return rooms.get(room.name).a.roomSize; } private PageAction showExamProctoringRoom( final ProctoringSettings proctoringSettings, final RemoteProctoringRoom room, - final Map rooms, final PageAction action) { - final int actualRoomSize = getActualRoomSize(room, rooms); - if (actualRoomSize <= 0) { - return action; - } - final SEBProctoringConnectionData proctoringConnectionData = this.pageService .getRestService() .getBuilder(GetProctorRoomConnectionData.class) diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/JitsiMeetProctoringView.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/JitsiMeetProctoringView.java index 2638d0b3..c78c6445 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/JitsiMeetProctoringView.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/JitsiMeetProctoringView.java @@ -17,6 +17,8 @@ import org.eclipse.swt.layout.RowData; import org.eclipse.swt.layout.RowLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @@ -41,6 +43,8 @@ import ch.ethz.seb.sebserver.gui.widget.WidgetFactory; @GuiProfile public class JitsiMeetProctoringView implements RemoteProctoringView { + private static final Logger log = LoggerFactory.getLogger(JitsiMeetProctoringView.class); + private static final LocTextKey CLOSE_WINDOW_TEXT_KEY = new LocTextKey("sebserver.monitoring.exam.proctoring.action.close"); private static final LocTextKey BROADCAST_AUDIO_ON_TEXT_KEY = @@ -92,8 +96,17 @@ public class JitsiMeetProctoringView implements RemoteProctoringView { .getProctoringGUIService() .closeRoom(proctoringWindowData.connectionData.roomName)); - final String url = this.guiServiceInfo.getExternalServerURIBuilder().toUriString() - + this.remoteProctoringEndpoint + this.remoteProctoringViewServletEndpoint + "/"; + final String url = this.guiServiceInfo + .getExternalServerURIBuilder() + .toUriString() + + this.remoteProctoringEndpoint + + this.remoteProctoringViewServletEndpoint + + Constants.SLASH; + + if (log.isDebugEnabled()) { + log.debug("Open proctoring Servlet in IFrame with URL: {}", url); + } + final Browser browser = new Browser(content, SWT.NONE | SWT.NO_SCROLL); browser.setLayout(new GridLayout()); final GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, true); @@ -274,7 +287,7 @@ public class JitsiMeetProctoringView implements RemoteProctoringView { return ProctoringServerType.JITSI_MEET; } - private static class BroadcastActionState { + private static final class BroadcastActionState { public static final String KEY_NAME = "BroadcastActionState"; boolean audio = false; boolean video = false; 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 5045803f..6842d395 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 @@ -410,68 +410,13 @@ public class ExamMonitoringController { this.authorization.checkRead( this.examSessionService.getExamDAO().byPK(examId).getOrThrow()); - final Map attributes = new HashMap<>(); - if (BooleanUtils.isTrue(sendReceiveAudio)) { - attributes.put( - ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_RECONFIGURE_SETTINGS.JITSI_RECEIVE_AUDIO, - Constants.TRUE_STRING); - } - if (BooleanUtils.isTrue(sendReceiveVideo)) { - attributes.put( - ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_RECONFIGURE_SETTINGS.JITSI_RECEIVE_VIDEO, - Constants.TRUE_STRING); - } - if (BooleanUtils.isTrue(sendAllowChat)) { - attributes.put( - ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_RECONFIGURE_SETTINGS.JITSI_ALLOW_CHAT, - Constants.TRUE_STRING); - } + final Map attributes = createProctorInstructionAttributes( + sendReceiveAudio, + sendReceiveVideo, + sendAllowChat, + Constants.TRUE_STRING); - 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"); - } + sendProctoringInstructions(examId, roomName, connectionTokens, attributes); } @RequestMapping( @@ -510,68 +455,18 @@ public class ExamMonitoringController { this.authorization.checkRead( this.examSessionService.getExamDAO().byPK(examId).getOrThrow()); - final Map attributes = new HashMap<>(); - if (BooleanUtils.isTrue(sendReceiveAudio)) { - attributes.put( - ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_RECONFIGURE_SETTINGS.JITSI_RECEIVE_AUDIO, - Constants.FALSE_STRING); - } - if (BooleanUtils.isTrue(sendReceiveVideo)) { - attributes.put( - ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_RECONFIGURE_SETTINGS.JITSI_RECEIVE_VIDEO, - Constants.FALSE_STRING); - } - if (BooleanUtils.isTrue(sendAllowChat)) { - attributes.put( - ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_RECONFIGURE_SETTINGS.JITSI_ALLOW_CHAT, - Constants.FALSE_STRING); - } + 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; } - 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"); - } + sendProctoringInstructions(examId, roomName, connectionTokens, attributes); } @RequestMapping( @@ -695,6 +590,83 @@ public class ExamMonitoringController { .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 //***********************************************************************************************