Merge remote-tracking branch 'origin/dev-1.1.0' into development
Conflicts: src/main/java/ch/ethz/seb/sebserver/gui/content/MonitoringRunningExam.java src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/JitsiMeetProctoringView.java src/main/java/ch/ethz/seb/sebserver/gui/service/session/ProctoringGUIService.java src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/RemoteProctoringRoomDAO.java src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/RemoteProctoringRoomDAOImpl.java src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamProctoringRoomService.java src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamProctoringRoomServiceImpl.java src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamProctoringController.java
This commit is contained in:
		
						commit
						6ff8b703c9
					
				
					 13 changed files with 402 additions and 194 deletions
				
			
		|  | @ -40,6 +40,9 @@ import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; | |||
| @Order(7) | ||||
| public class WebSecurityConfig extends WebSecurityConfigurerAdapter implements ErrorController { | ||||
| 
 | ||||
|     private static final String ERROR_PATH = "/sebserver/error"; | ||||
|     private static final String CHECK_PATH = "/sebserver/check"; | ||||
| 
 | ||||
|     @Value("${sebserver.webservice.http.redirect.gui}") | ||||
|     private String guiRedirect; | ||||
|     @Value("${sebserver.webservice.api.exam.endpoint.discovery}") | ||||
|  | @ -78,22 +81,27 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter implements E | |||
|     public void configure(final WebSecurity web) { | ||||
|         web | ||||
|                 .ignoring() | ||||
|                 .antMatchers("/error") | ||||
|                 .antMatchers(ERROR_PATH) | ||||
|                 .antMatchers(CHECK_PATH) | ||||
|                 .antMatchers(this.examAPIDiscoveryEndpoint) | ||||
|                 .antMatchers(this.adminAPIEndpoint + API.INFO_ENDPOINT + API.LOGO_PATH_SEGMENT + "/**") | ||||
|                 .antMatchers(this.adminAPIEndpoint + API.INFO_ENDPOINT + API.INFO_INST_PATH_SEGMENT + "/**") | ||||
|                 .antMatchers(this.adminAPIEndpoint + API.REGISTER_ENDPOINT); | ||||
|     } | ||||
| 
 | ||||
|     @RequestMapping("/error") | ||||
|     @RequestMapping(CHECK_PATH) | ||||
|     public void check() throws IOException { | ||||
|     } | ||||
| 
 | ||||
|     @RequestMapping(ERROR_PATH) | ||||
|     public void handleError(final HttpServletResponse response) throws IOException { | ||||
|         //response.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY); | ||||
|         response.getOutputStream().print(response.getStatus()); | ||||
|         response.setHeader(HttpHeaders.LOCATION, this.guiRedirect); | ||||
|         response.flushBuffer(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public String getErrorPath() { | ||||
|         return "/error"; | ||||
|         return ERROR_PATH; | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -185,6 +185,7 @@ public final class API { | |||
|     public static final String EXAM_PROCTORING_ROOM_CONNECTIONS_PATH_SEGMENT = "/room-connections"; | ||||
|     public static final String EXAM_PROCTORING_ACTIVATE_TOWNHALL_ROOM = "activate-towhall-room"; | ||||
|     public static final String EXAM_PROCTORING_TOWNHALL_ROOM_DATA = "towhall-room-data"; | ||||
|     public static final String EXAM_PROCTORING_TOWNHALL_ROOM_AVAILABLE = "towhall-available"; | ||||
| 
 | ||||
|     public static final String EXAM_PROCTORING_ATTR_RECEIVE_AUDIO = "receive_audio"; | ||||
|     public static final String EXAM_PROCTORING_ATTR_RECEIVE_VIDEO = "receive_video"; | ||||
|  |  | |||
|  | @ -116,60 +116,56 @@ public final class InstitutionalAuthenticationEntryPoint implements Authenticati | |||
| 
 | ||||
|         final String institutionalEndpoint = extractInstitutionalEndpoint(request); | ||||
| 
 | ||||
|         if (StringUtils.isNoneBlank(institutionalEndpoint) && log.isDebugEnabled()) { | ||||
|             log.debug("No default gui entrypoint requested: {}", institutionalEndpoint); | ||||
|         } else { | ||||
|             request.getSession().setAttribute(INST_SUFFIX_ATTRIBUTE, null); | ||||
|             request.getSession().removeAttribute(API.PARAM_LOGO_IMAGE); | ||||
|             forwardToEntryPoint(request, response, this.guiEntryPoint, false); | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         try { | ||||
| 
 | ||||
|             final RestTemplate restTemplate = new RestTemplate(); | ||||
|             final List<EntityName> institutions = restTemplate | ||||
|                     .exchange( | ||||
|                             this.webserviceURIService.getURIBuilder() | ||||
|                                     .path(API.INFO_ENDPOINT + API.INFO_INST_ENDPOINT) | ||||
|                                     .toUriString(), | ||||
|                             HttpMethod.GET, | ||||
|                             HttpEntity.EMPTY, | ||||
|                             new ParameterizedTypeReference<List<EntityName>>() { | ||||
|                             }, | ||||
|                             institutionalEndpoint, | ||||
|                             API.INFO_PARAM_INST_SUFFIX, | ||||
|                             institutionalEndpoint) | ||||
|                     .getBody(); | ||||
| 
 | ||||
|             if (institutions != null && !institutions.isEmpty()) { | ||||
|                 request.getSession().setAttribute( | ||||
|                         INST_SUFFIX_ATTRIBUTE, | ||||
|                         StringUtils.isNotBlank(institutionalEndpoint) | ||||
|                                 ? institutionalEndpoint | ||||
|                                 : null); | ||||
| 
 | ||||
|                 if (log.isDebugEnabled()) { | ||||
|                     log.debug("Known and active gui entrypoint requested: {}", institutions); | ||||
|                 } | ||||
| 
 | ||||
|                 final String logoImageBase64 = requestLogoImage(institutionalEndpoint); | ||||
|                 if (StringUtils.isNotBlank(logoImageBase64)) { | ||||
|                     request.getSession().setAttribute(API.PARAM_LOGO_IMAGE, logoImageBase64); | ||||
| 
 | ||||
|                 } | ||||
|                 forwardToEntryPoint(request, response, this.guiEntryPoint, false); | ||||
|                 return; | ||||
|         if (StringUtils.isNotBlank(institutionalEndpoint)) { | ||||
|             if (log.isDebugEnabled()) { | ||||
|                 log.debug("No default gui entrypoint requested: {}", institutionalEndpoint); | ||||
|             } | ||||
|         } catch (final Exception e) { | ||||
|             log.error("Failed to extract and set institutional endpoint request: ", e); | ||||
| 
 | ||||
|             try { | ||||
| 
 | ||||
|                 final RestTemplate restTemplate = new RestTemplate(); | ||||
|                 final List<EntityName> institutions = restTemplate | ||||
|                         .exchange( | ||||
|                                 this.webserviceURIService.getURIBuilder() | ||||
|                                         .path(API.INFO_ENDPOINT + API.INFO_INST_ENDPOINT) | ||||
|                                         .toUriString(), | ||||
|                                 HttpMethod.GET, | ||||
|                                 HttpEntity.EMPTY, | ||||
|                                 new ParameterizedTypeReference<List<EntityName>>() { | ||||
|                                 }, | ||||
|                                 institutionalEndpoint, | ||||
|                                 API.INFO_PARAM_INST_SUFFIX, | ||||
|                                 institutionalEndpoint) | ||||
|                         .getBody(); | ||||
| 
 | ||||
|                 if (institutions != null && !institutions.isEmpty()) { | ||||
|                     request.getSession().setAttribute( | ||||
|                             INST_SUFFIX_ATTRIBUTE, | ||||
|                             StringUtils.isNotBlank(institutionalEndpoint) | ||||
|                                     ? institutionalEndpoint | ||||
|                                     : null); | ||||
| 
 | ||||
|                     if (log.isDebugEnabled()) { | ||||
|                         log.debug("Known and active gui entrypoint requested: {}", institutions); | ||||
|                     } | ||||
| 
 | ||||
|                     final String logoImageBase64 = requestLogoImage(institutionalEndpoint); | ||||
|                     if (StringUtils.isNotBlank(logoImageBase64)) { | ||||
|                         request.getSession().setAttribute(API.PARAM_LOGO_IMAGE, logoImageBase64); | ||||
| 
 | ||||
|                     } | ||||
|                     forwardToEntryPoint(request, response, this.guiEntryPoint, false); | ||||
|                     return; | ||||
|                 } | ||||
|             } catch (final Exception e) { | ||||
|                 log.error("Failed to extract and set institutional endpoint request: ", e); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         request.getSession().setAttribute(INST_SUFFIX_ATTRIBUTE, null); | ||||
|         request.getSession().removeAttribute(API.PARAM_LOGO_IMAGE); | ||||
|         response.setStatus(HttpStatus.UNAUTHORIZED.value()); | ||||
|         forwardToEntryPoint(request, response, this.guiEntryPoint, true); | ||||
|         forwardToEntryPoint(request, response, this.guiEntryPoint, institutionalEndpoint == null); | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|  | @ -203,16 +199,25 @@ public final class InstitutionalAuthenticationEntryPoint implements Authenticati | |||
| 
 | ||||
|     public static String extractInstitutionalEndpoint(final HttpServletRequest request) { | ||||
|         final String requestURI = request.getRequestURI(); | ||||
|         if (StringUtils.isBlank(requestURI) || requestURI.equals(Constants.SLASH.toString())) { | ||||
|         if (StringUtils.isBlank(requestURI)) { | ||||
|             return null; | ||||
|         } | ||||
| 
 | ||||
|         if (log.isDebugEnabled()) { | ||||
|             log.debug("Trying to verify institution from requested entrypoint url: {}", requestURI); | ||||
|         if (requestURI.equals(Constants.SLASH.toString())) { | ||||
|             return StringUtils.EMPTY; | ||||
|         } | ||||
| 
 | ||||
|         try { | ||||
|             return requestURI.substring(requestURI.lastIndexOf(Constants.SLASH) + 1); | ||||
|             if (log.isDebugEnabled()) { | ||||
|                 log.debug("Trying to verify institution from requested entrypoint url: {}", requestURI); | ||||
|             } | ||||
| 
 | ||||
|             final String[] split = StringUtils.split(requestURI, Constants.SLASH); | ||||
|             if (split.length > 1) { | ||||
|                 return null; | ||||
|             } | ||||
| 
 | ||||
|             return split[0]; | ||||
|         } catch (final Exception e) { | ||||
|             log.error("Failed to extract institutional URL suffix: {}", e.getMessage()); | ||||
|             return null; | ||||
|  |  | |||
|  | @ -17,6 +17,7 @@ import java.util.function.BooleanSupplier; | |||
| import java.util.function.Consumer; | ||||
| import java.util.function.Function; | ||||
| 
 | ||||
| import org.apache.commons.lang3.BooleanUtils; | ||||
| import org.eclipse.rap.rwt.RWT; | ||||
| import org.eclipse.rap.rwt.client.service.JavaScriptExecutor; | ||||
| import org.eclipse.swt.SWT; | ||||
|  | @ -77,7 +78,7 @@ import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetProctorin | |||
| import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetClientConnectionDataList; | ||||
| import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetCollectingRooms; | ||||
| import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetProctorRoomConnection; | ||||
| import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetTownhallRoom; | ||||
| import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.IsTownhallRoomAvailable; | ||||
| import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser; | ||||
| import ch.ethz.seb.sebserver.gui.service.session.ClientConnectionTable; | ||||
| import ch.ethz.seb.sebserver.gui.service.session.InstructionProcessor; | ||||
|  | @ -123,6 +124,7 @@ public class MonitoringRunningExam implements TemplateComposer { | |||
| 
 | ||||
|     private final ServerPushService serverPushService; | ||||
|     private final PageService pageService; | ||||
|     private final RestService restService; | ||||
|     private final ResourceService resourceService; | ||||
|     private final AsyncRunner asyncRunner; | ||||
|     private final InstructionProcessor instructionProcessor; | ||||
|  | @ -147,6 +149,7 @@ public class MonitoringRunningExam implements TemplateComposer { | |||
| 
 | ||||
|         this.serverPushService = serverPushService; | ||||
|         this.pageService = pageService; | ||||
|         this.restService = pageService.getRestService(); | ||||
|         this.resourceService = pageService.getResourceService(); | ||||
|         this.asyncRunner = asyncRunner; | ||||
|         this.instructionProcessor = instructionProcessor; | ||||
|  | @ -214,13 +217,14 @@ public class MonitoringRunningExam implements TemplateComposer { | |||
| 
 | ||||
|         this.serverPushService.runServerPush( | ||||
|                 new ServerPushContext( | ||||
|                         content, Utils.truePredicate(), | ||||
|                         content, | ||||
|                         Utils.truePredicate(), | ||||
|                         createServerPushUpdateErrorHandler(this.pageService, pageContext)), | ||||
|                 this.pollInterval, | ||||
|                 context -> clientTable.updateValues(), | ||||
|                 updateTableGUI(clientTable)); | ||||
| 
 | ||||
|         final BooleanSupplier privilege = () -> currentUser.get().hasRole(UserRole.EXAM_SUPPORTER); | ||||
|         final BooleanSupplier isExamSupporter = () -> currentUser.get().hasRole(UserRole.EXAM_SUPPORTER); | ||||
| 
 | ||||
|         actionBuilder | ||||
| 
 | ||||
|  | @ -242,20 +246,20 @@ public class MonitoringRunningExam implements TemplateComposer { | |||
| 
 | ||||
|                     return copyOfPageAction; | ||||
|                 }) | ||||
|                 .publishIf(privilege, false) | ||||
|                 .publishIf(isExamSupporter, false) | ||||
| 
 | ||||
|                 .newAction(ActionDefinition.MONITOR_EXAM_QUIT_ALL) | ||||
|                 .withEntityKey(entityKey) | ||||
|                 .withConfirm(() -> CONFIRM_QUIT_ALL) | ||||
|                 .withExec(action -> this.quitSEBClients(action, clientTable, true)) | ||||
|                 .noEventPropagation() | ||||
|                 .publishIf(privilege) | ||||
|                 .publishIf(isExamSupporter) | ||||
| 
 | ||||
|                 .newAction(ActionDefinition.MONITORING_EXAM_SEARCH_CONNECTIONS) | ||||
|                 .withEntityKey(entityKey) | ||||
|                 .withExec(this::openSearchPopup) | ||||
|                 .noEventPropagation() | ||||
|                 .publishIf(privilege) | ||||
|                 .publishIf(isExamSupporter) | ||||
| 
 | ||||
|                 .newAction(ActionDefinition.MONITOR_EXAM_QUIT_SELECTED) | ||||
|                 .withEntityKey(entityKey) | ||||
|  | @ -265,7 +269,7 @@ public class MonitoringRunningExam implements TemplateComposer { | |||
|                         action -> this.quitSEBClients(action, clientTable, false), | ||||
|                         EMPTY_ACTIVE_SELECTION_TEXT_KEY) | ||||
|                 .noEventPropagation() | ||||
|                 .publishIf(privilege, false) | ||||
|                 .publishIf(isExamSupporter, false) | ||||
| 
 | ||||
|                 .newAction(ActionDefinition.MONITOR_EXAM_DISABLE_SELECTED_CONNECTION) | ||||
|                 .withEntityKey(entityKey) | ||||
|  | @ -275,81 +279,26 @@ public class MonitoringRunningExam implements TemplateComposer { | |||
|                         action -> this.disableSEBClients(action, clientTable, false), | ||||
|                         EMPTY_SELECTION_TEXT_KEY) | ||||
|                 .noEventPropagation() | ||||
|                 .publishIf(privilege, false); | ||||
| 
 | ||||
|         if (privilege.getAsBoolean()) { | ||||
| 
 | ||||
|             if (clientTable.isStatusHidden(ConnectionStatus.CLOSED)) { | ||||
|                 actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_CLOSED_CONNECTION) | ||||
|                         .withExec(showStateViewAction(clientTable, ConnectionStatus.CLOSED)) | ||||
|                         .noEventPropagation() | ||||
|                         .withSwitchAction( | ||||
|                                 actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_CLOSED_CONNECTION) | ||||
|                                         .withExec(hideStateViewAction(clientTable, ConnectionStatus.CLOSED)) | ||||
|                                         .noEventPropagation() | ||||
|                                         .create()) | ||||
|                         .publish(); | ||||
|             } else { | ||||
|                 actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_CLOSED_CONNECTION) | ||||
|                         .withExec(hideStateViewAction(clientTable, ConnectionStatus.CLOSED)) | ||||
|                         .noEventPropagation() | ||||
|                         .withSwitchAction( | ||||
|                                 actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_CLOSED_CONNECTION) | ||||
|                                         .withExec(showStateViewAction(clientTable, ConnectionStatus.CLOSED)) | ||||
|                                         .noEventPropagation() | ||||
|                                         .create()) | ||||
|                         .publish(); | ||||
|             } | ||||
| 
 | ||||
|             if (clientTable.isStatusHidden(ConnectionStatus.CONNECTION_REQUESTED)) { | ||||
|                 actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_REQUESTED_CONNECTION) | ||||
|                         .withExec(showStateViewAction(clientTable, ConnectionStatus.CONNECTION_REQUESTED)) | ||||
|                         .noEventPropagation() | ||||
|                         .withSwitchAction( | ||||
|                                 actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_REQUESTED_CONNECTION) | ||||
|                                         .withExec( | ||||
|                                                 hideStateViewAction(clientTable, ConnectionStatus.CONNECTION_REQUESTED)) | ||||
|                                         .noEventPropagation() | ||||
|                                         .create()) | ||||
|                         .publish(); | ||||
|             } else { | ||||
|                 actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_REQUESTED_CONNECTION) | ||||
|                         .withExec(hideStateViewAction(clientTable, ConnectionStatus.CONNECTION_REQUESTED)) | ||||
|                         .noEventPropagation() | ||||
|                         .withSwitchAction( | ||||
|                                 actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_REQUESTED_CONNECTION) | ||||
|                                         .withExec( | ||||
|                                                 showStateViewAction(clientTable, ConnectionStatus.CONNECTION_REQUESTED)) | ||||
|                                         .noEventPropagation() | ||||
|                                         .create()) | ||||
|                         .publish(); | ||||
|             } | ||||
| 
 | ||||
|             if (clientTable.isStatusHidden(ConnectionStatus.DISABLED)) { | ||||
|                 actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_DISABLED_CONNECTION) | ||||
|                         .withExec(showStateViewAction(clientTable, ConnectionStatus.DISABLED)) | ||||
|                         .noEventPropagation() | ||||
|                         .withSwitchAction( | ||||
|                                 actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_DISABLED_CONNECTION) | ||||
|                                         .withExec(hideStateViewAction(clientTable, ConnectionStatus.DISABLED)) | ||||
|                                         .noEventPropagation() | ||||
|                                         .create()) | ||||
|                         .publish(); | ||||
|             } else { | ||||
|                 actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_DISABLED_CONNECTION) | ||||
|                         .withExec(hideStateViewAction(clientTable, ConnectionStatus.DISABLED)) | ||||
|                         .noEventPropagation() | ||||
|                         .withSwitchAction( | ||||
|                                 actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_DISABLED_CONNECTION) | ||||
|                                         .withExec(showStateViewAction(clientTable, ConnectionStatus.DISABLED)) | ||||
|                                         .noEventPropagation() | ||||
|                                         .create()) | ||||
|                         .publish(); | ||||
|             } | ||||
|                 .publishIf(isExamSupporter, false); | ||||
| 
 | ||||
|         if (isExamSupporter.getAsBoolean()) { | ||||
|             addFilterActions(actionBuilder, clientTable, isExamSupporter); | ||||
|             addProctoringActions( | ||||
|                     currentUser.getProctoringGUIService(), | ||||
|                     pageContext, | ||||
|                     content, | ||||
|                     actionBuilder); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|         final ProctoringServiceSettings proctoringSettings = restService | ||||
|     private void addProctoringActions( | ||||
|             final ProctoringGUIService proctoringGUIService, | ||||
|             final PageContext pageContext, | ||||
|             final Composite parent, | ||||
|             final PageActionBuilder actionBuilder) { | ||||
| 
 | ||||
|         final EntityKey entityKey = pageContext.getEntityKey(); | ||||
|         final ProctoringServiceSettings proctoringSettings = this.restService | ||||
|                 .getBuilder(GetProctoringSettings.class) | ||||
|                 .withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId) | ||||
|                 .call() | ||||
|  | @ -359,7 +308,7 @@ public class MonitoringRunningExam implements TemplateComposer { | |||
| 
 | ||||
|             actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM) | ||||
|                     .withEntityKey(entityKey) | ||||
|                     .withExec(this::toggleTownhallRoom) | ||||
|                     .withExec(action -> this.toggleTownhallRoom(proctoringGUIService, action)) | ||||
|                     .noEventPropagation() | ||||
|                     .publish(); | ||||
| 
 | ||||
|  | @ -375,34 +324,126 @@ public class MonitoringRunningExam implements TemplateComposer { | |||
| 
 | ||||
|             final Map<String, Pair<RemoteProctoringRoom, TreeItem>> availableRooms = new HashMap<>(); | ||||
|             updateRoomActions( | ||||
|                     entityKey, | ||||
|                     pageContext, | ||||
|                     availableRooms, | ||||
|                     actionBuilder, | ||||
|                     proctoringSettings); | ||||
|                     proctoringSettings, | ||||
|                     proctoringGUIService); | ||||
|             this.serverPushService.runServerPush( | ||||
|                     new ServerPushContext( | ||||
|                             content, | ||||
|                             parent, | ||||
|                             Utils.truePredicate(), | ||||
|                             createServerPushUpdateErrorHandler(this.pageService, pageContext)), | ||||
|                     this.proctoringRoomUpdateInterval, | ||||
|                     context -> updateRoomActions( | ||||
|                             entityKey, | ||||
|                             pageContext, | ||||
|                             availableRooms, | ||||
|                             actionBuilder, | ||||
|                             proctoringSettings)); | ||||
|                             proctoringSettings, | ||||
|                             proctoringGUIService)); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private void addFilterActions( | ||||
|             final PageActionBuilder actionBuilder, | ||||
|             final ClientConnectionTable clientTable, | ||||
|             final BooleanSupplier isExamSupporter) { | ||||
| 
 | ||||
|         addClosedFilterAction(actionBuilder, clientTable); | ||||
|         addRequestedFilterAction(actionBuilder, clientTable); | ||||
|         addDisabledFilterAction(actionBuilder, clientTable); | ||||
|     } | ||||
| 
 | ||||
|     private void addDisabledFilterAction( | ||||
|             final PageActionBuilder actionBuilder, | ||||
|             final ClientConnectionTable clientTable) { | ||||
| 
 | ||||
|         if (clientTable.isStatusHidden(ConnectionStatus.DISABLED)) { | ||||
|             actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_DISABLED_CONNECTION) | ||||
|                     .withExec(showStateViewAction(clientTable, ConnectionStatus.DISABLED)) | ||||
|                     .noEventPropagation() | ||||
|                     .withSwitchAction( | ||||
|                             actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_DISABLED_CONNECTION) | ||||
|                                     .withExec(hideStateViewAction(clientTable, ConnectionStatus.DISABLED)) | ||||
|                                     .noEventPropagation() | ||||
|                                     .create()) | ||||
|                     .publish(); | ||||
|         } else { | ||||
|             actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_DISABLED_CONNECTION) | ||||
|                     .withExec(hideStateViewAction(clientTable, ConnectionStatus.DISABLED)) | ||||
|                     .noEventPropagation() | ||||
|                     .withSwitchAction( | ||||
|                             actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_DISABLED_CONNECTION) | ||||
|                                     .withExec(showStateViewAction(clientTable, ConnectionStatus.DISABLED)) | ||||
|                                     .noEventPropagation() | ||||
|                                     .create()) | ||||
|                     .publish(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private void addRequestedFilterAction( | ||||
|             final PageActionBuilder actionBuilder, | ||||
|             final ClientConnectionTable clientTable) { | ||||
| 
 | ||||
|         if (clientTable.isStatusHidden(ConnectionStatus.CONNECTION_REQUESTED)) { | ||||
|             actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_REQUESTED_CONNECTION) | ||||
|                     .withExec(showStateViewAction(clientTable, ConnectionStatus.CONNECTION_REQUESTED)) | ||||
|                     .noEventPropagation() | ||||
|                     .withSwitchAction( | ||||
|                             actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_REQUESTED_CONNECTION) | ||||
|                                     .withExec( | ||||
|                                             hideStateViewAction(clientTable, ConnectionStatus.CONNECTION_REQUESTED)) | ||||
|                                     .noEventPropagation() | ||||
|                                     .create()) | ||||
|                     .publish(); | ||||
|         } else { | ||||
|             actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_REQUESTED_CONNECTION) | ||||
|                     .withExec(hideStateViewAction(clientTable, ConnectionStatus.CONNECTION_REQUESTED)) | ||||
|                     .noEventPropagation() | ||||
|                     .withSwitchAction( | ||||
|                             actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_REQUESTED_CONNECTION) | ||||
|                                     .withExec( | ||||
|                                             showStateViewAction(clientTable, ConnectionStatus.CONNECTION_REQUESTED)) | ||||
|                                     .noEventPropagation() | ||||
|                                     .create()) | ||||
|                     .publish(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private void addClosedFilterAction( | ||||
|             final PageActionBuilder actionBuilder, | ||||
|             final ClientConnectionTable clientTable) { | ||||
| 
 | ||||
|         if (clientTable.isStatusHidden(ConnectionStatus.CLOSED)) { | ||||
|             actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_CLOSED_CONNECTION) | ||||
|                     .withExec(showStateViewAction(clientTable, ConnectionStatus.CLOSED)) | ||||
|                     .noEventPropagation() | ||||
|                     .withSwitchAction( | ||||
|                             actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_CLOSED_CONNECTION) | ||||
|                                     .withExec(hideStateViewAction(clientTable, ConnectionStatus.CLOSED)) | ||||
|                                     .noEventPropagation() | ||||
|                                     .create()) | ||||
|                     .publish(); | ||||
|         } else { | ||||
|             actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_CLOSED_CONNECTION) | ||||
|                     .withExec(hideStateViewAction(clientTable, ConnectionStatus.CLOSED)) | ||||
|                     .noEventPropagation() | ||||
|                     .withSwitchAction( | ||||
|                             actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_CLOSED_CONNECTION) | ||||
|                                     .withExec(showStateViewAction(clientTable, ConnectionStatus.CLOSED)) | ||||
|                                     .noEventPropagation() | ||||
|                                     .create()) | ||||
|                     .publish(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private boolean isTownhallRoomActive(final String examModelId) { | ||||
|         final RemoteProctoringRoom townhall = this.pageService.getRestService() | ||||
|                 .getBuilder(GetTownhallRoom.class) | ||||
|         return !BooleanUtils.toBoolean(this.pageService | ||||
|                 .getRestService() | ||||
|                 .getBuilder(IsTownhallRoomAvailable.class) | ||||
|                 .withURIVariable(API.PARAM_MODEL_ID, examModelId) | ||||
|                 .call() | ||||
|                 .getOr(null); | ||||
| 
 | ||||
|         return townhall != null && townhall.id != null; | ||||
|                 .getOr(Constants.FALSE_STRING)); | ||||
|     } | ||||
| 
 | ||||
|     private PageAction openSearchPopup(final PageAction action) { | ||||
|  | @ -410,9 +451,12 @@ public class MonitoringRunningExam implements TemplateComposer { | |||
|         return action; | ||||
|     } | ||||
| 
 | ||||
|     private PageAction toggleTownhallRoom(final PageAction action) { | ||||
|     private PageAction toggleTownhallRoom( | ||||
|             final ProctoringGUIService proctoringGUIService, | ||||
|             final PageAction action) { | ||||
| 
 | ||||
|         if (isTownhallRoomActive(action.getEntityKey().modelId)) { | ||||
|             closeTownhallRoom(action); | ||||
|             closeTownhallRoom(proctoringGUIService, action); | ||||
|             this.pageService.firePageEvent( | ||||
|                     new ActionActivationEvent( | ||||
|                             true, | ||||
|  | @ -422,7 +466,7 @@ public class MonitoringRunningExam implements TemplateComposer { | |||
|                     action.pageContext()); | ||||
|             return action; | ||||
|         } else { | ||||
|             openTownhallRoom(action); | ||||
|             openTownhallRoom(proctoringGUIService, action); | ||||
|             this.pageService.firePageEvent( | ||||
|                     new ActionActivationEvent( | ||||
|                             true, | ||||
|  | @ -434,14 +478,13 @@ public class MonitoringRunningExam implements TemplateComposer { | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private PageAction openTownhallRoom(final PageAction action) { | ||||
|     private PageAction openTownhallRoom( | ||||
|             final ProctoringGUIService proctoringGUIService, | ||||
|             final PageAction action) { | ||||
| 
 | ||||
|         try { | ||||
|             final EntityKey examId = action.getEntityKey(); | ||||
| 
 | ||||
|             final ProctoringGUIService proctoringGUIService = this.pageService | ||||
|                     .getCurrentUser() | ||||
|                     .getProctoringGUIService(); | ||||
| 
 | ||||
|             final String windowName = getTownhallWindowName(examId.modelId); | ||||
|             if (!proctoringGUIService.hasWindow(windowName)) { | ||||
|                 final ProctoringRoomConnection proctoringConnectionData = proctoringGUIService | ||||
|  | @ -475,11 +518,10 @@ public class MonitoringRunningExam implements TemplateComposer { | |||
|         return action; | ||||
|     } | ||||
| 
 | ||||
|     private String getTownhallWindowName(final String examId) { | ||||
|         return examId + "_townhall"; | ||||
|     } | ||||
|     private PageAction closeTownhallRoom( | ||||
|             final ProctoringGUIService proctoringGUIService, | ||||
|             final PageAction action) { | ||||
| 
 | ||||
|     private PageAction closeTownhallRoom(final PageAction action) { | ||||
|         final String examId = action.getEntityKey().modelId; | ||||
|         try { | ||||
| 
 | ||||
|  | @ -494,34 +536,52 @@ public class MonitoringRunningExam implements TemplateComposer { | |||
|         return action; | ||||
|     } | ||||
| 
 | ||||
|     private void updateTownhallButton(final EntityKey entityKey, final PageContext pageContext) { | ||||
|     private void updateTownhallButton( | ||||
|             final ProctoringGUIService proctoringGUIService, | ||||
|             final PageContext pageContext) { | ||||
|         final EntityKey entityKey = pageContext.getEntityKey(); | ||||
| 
 | ||||
|         if (isTownhallRoomActive(entityKey.modelId)) { | ||||
|             this.pageService.firePageEvent( | ||||
|                     new ActionActivationEvent( | ||||
|                             true, | ||||
|                             new Tuple<>( | ||||
|                                     ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM, | ||||
|                                     ActionDefinition.MONITOR_EXAM_CLOSE_TOWNHALL_PROCTOR_ROOM)), | ||||
|                     pageContext); | ||||
|             final boolean townhallRoomFromThisUser = proctoringGUIService | ||||
|                     .isTownhallOpenForUser(entityKey.modelId); | ||||
|             if (townhallRoomFromThisUser) { | ||||
|                 this.pageService.firePageEvent( | ||||
|                         new ActionActivationEvent( | ||||
|                                 true, | ||||
|                                 new Tuple<>( | ||||
|                                         ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM, | ||||
|                                         ActionDefinition.MONITOR_EXAM_CLOSE_TOWNHALL_PROCTOR_ROOM)), | ||||
|                         pageContext); | ||||
|             } else { | ||||
|                 this.pageService.firePageEvent( | ||||
|                         new ActionActivationEvent( | ||||
|                                 false, | ||||
|                                 ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM, | ||||
|                                 ActionDefinition.MONITOR_EXAM_CLOSE_TOWNHALL_PROCTOR_ROOM), | ||||
|                         pageContext); | ||||
|             } | ||||
|         } else { | ||||
|             this.pageService.firePageEvent( | ||||
|                     new ActionActivationEvent( | ||||
|                             true, | ||||
|                             new Tuple<>( | ||||
|                                     ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM, | ||||
|                                     ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM)), | ||||
|                                     ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM), | ||||
|                             ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM, | ||||
|                             ActionDefinition.MONITOR_EXAM_CLOSE_TOWNHALL_PROCTOR_ROOM), | ||||
|                     pageContext); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private void updateRoomActions( | ||||
|             final EntityKey entityKey, | ||||
|             final PageContext pageContext, | ||||
|             final Map<String, Pair<RemoteProctoringRoom, TreeItem>> rooms, | ||||
|             final PageActionBuilder actionBuilder, | ||||
|             final ProctoringServiceSettings proctoringSettings) { | ||||
|             final ProctoringServiceSettings proctoringSettings, | ||||
|             final ProctoringGUIService proctoringGUIService) { | ||||
| 
 | ||||
|         updateTownhallButton(entityKey, pageContext); | ||||
|         final EntityKey entityKey = pageContext.getEntityKey(); | ||||
|         updateTownhallButton(proctoringGUIService, pageContext); | ||||
|         final I18nSupport i18nSupport = this.pageService.getI18nSupport(); | ||||
|         this.pageService | ||||
|                 .getRestService() | ||||
|  | @ -564,7 +624,7 @@ public class MonitoringRunningExam implements TemplateComposer { | |||
|                         this.pageService.publishAction( | ||||
|                                 action, | ||||
|                                 _treeItem -> rooms.put(room.name, new Pair<>(room, _treeItem))); | ||||
|                         addRoomConnectionsPopupListener(entityKey, pageContext, rooms); | ||||
|                         addRoomConnectionsPopupListener(pageContext, rooms); | ||||
|                         processProctorRoomActionActivation(rooms.get(room.name).b, room, pageContext); | ||||
|                     } | ||||
|                 }); | ||||
|  | @ -589,11 +649,11 @@ public class MonitoringRunningExam implements TemplateComposer { | |||
|     } | ||||
| 
 | ||||
|     private void addRoomConnectionsPopupListener( | ||||
|             final EntityKey entityKey, | ||||
|             final PageContext pageContext, | ||||
|             final Map<String, Pair<RemoteProctoringRoom, TreeItem>> rooms) { | ||||
| 
 | ||||
|         if (!rooms.isEmpty()) { | ||||
|             final EntityKey entityKey = pageContext.getEntityKey(); | ||||
|             final TreeItem treeItem = rooms.values().iterator().next().b; | ||||
|             final Tree tree = treeItem.getParent(); | ||||
|             if (tree.getData(SHOW_CONNECTION_ACTION_APPLIED) == null) { | ||||
|  | @ -775,4 +835,8 @@ public class MonitoringRunningExam implements TemplateComposer { | |||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     private String getTownhallWindowName(final String examId) { | ||||
|         return examId + "_townhall"; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -84,17 +84,18 @@ public class JitsiMeetProctoringView implements RemoteProctoringView { | |||
|     public void compose(final PageContext pageContext) { | ||||
| 
 | ||||
|         final ProctoringWindowData proctoringWindowData = ProctoringGUIService.getCurrentProctoringWindowData(); | ||||
| 
 | ||||
|         final Composite parent = pageContext.getParent(); | ||||
| 
 | ||||
|         final Composite content = new Composite(parent, SWT.NONE | SWT.NO_SCROLL); | ||||
|         final GridLayout gridLayout = new GridLayout(); | ||||
|         final ProctoringGUIService proctoringGUIService = this.pageService | ||||
|                 .getCurrentUser() | ||||
|                 .getProctoringGUIService(); | ||||
| 
 | ||||
|         content.setLayout(gridLayout); | ||||
|         final GridData headerCell = new GridData(SWT.FILL, SWT.FILL, true, true); | ||||
|         content.setLayoutData(headerCell); | ||||
| 
 | ||||
|         parent.addListener(SWT.Dispose, event -> closeRoom(proctoringWindowData)); | ||||
|         parent.addListener(SWT.Dispose, event -> closeRoom(proctoringGUIService, proctoringWindowData)); | ||||
| 
 | ||||
|         final String url = this.guiServiceInfo | ||||
|                 .getExternalServerURIBuilder() | ||||
|  | @ -124,7 +125,7 @@ public class JitsiMeetProctoringView implements RemoteProctoringView { | |||
| 
 | ||||
|         final Button closeAction = widgetFactory.buttonLocalized(footer, CLOSE_WINDOW_TEXT_KEY); | ||||
|         closeAction.setLayoutData(new RowData(150, 30)); | ||||
|         closeAction.addListener(SWT.Selection, event -> closeRoom(proctoringWindowData)); | ||||
|         closeAction.addListener(SWT.Selection, event -> closeRoom(proctoringGUIService, proctoringWindowData)); | ||||
| 
 | ||||
|         final BroadcastActionState broadcastActionState = new BroadcastActionState(); | ||||
| 
 | ||||
|  | @ -232,11 +233,15 @@ public class JitsiMeetProctoringView implements RemoteProctoringView { | |||
|         sendReconfigurationAttributes(examId, roomName, state); | ||||
|     } | ||||
| 
 | ||||
|     private void closeRoom(final ProctoringWindowData proctoringWindowData) { | ||||
|         this.pageService | ||||
|                 .getCurrentUser() | ||||
|                 .getProctoringGUIService() | ||||
|                 .closeRoomWindow(proctoringWindowData.windowName); | ||||
|     private void closeRoom( | ||||
|             final ProctoringGUIService proctoringGUIService, | ||||
|             final ProctoringWindowData proctoringWindowData) { | ||||
| 
 | ||||
|         try { | ||||
|             proctoringGUIService.closeRoomWindow(proctoringWindowData.connectionData.roomName); | ||||
|         } catch (final Exception e) { | ||||
|             log.error("Failed to close proctoring window properly: ", e); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     static final class BroadcastActionState { | ||||
|  |  | |||
|  | @ -0,0 +1,41 @@ | |||
| /* | ||||
|  * Copyright (c) 2021 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 IsTownhallRoomAvailable extends RestCall<String> { | ||||
| 
 | ||||
|     public IsTownhallRoomAvailable() { | ||||
|         super(new TypeKey<>( | ||||
|                 CallType.GET_SINGLE, | ||||
|                 EntityType.REMOTE_PROCTORING_ROOM, | ||||
|                 new TypeReference<String>() { | ||||
|                 }), | ||||
|                 HttpMethod.GET, | ||||
|                 MediaType.APPLICATION_FORM_URLENCODED, | ||||
|                 API.EXAM_PROCTORING_ENDPOINT | ||||
|                         + API.MODEL_ID_VAR_PATH_SEGMENT | ||||
|                         + API.EXAM_PROCTORING_TOWNHALL_ROOM_AVAILABLE); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -50,6 +50,13 @@ public class ProctoringGUIService { | |||
|         this.openWindows.put(windowName, new RoomData(roomName, examId)); | ||||
|     } | ||||
| 
 | ||||
|     public boolean isTownhallOpenForUser(final String examId) { | ||||
|         return this.openWindows.values().stream() | ||||
|                 .filter(room -> room.isTownhall && room.examId.equals(examId)) | ||||
|                 .findFirst() | ||||
|                 .isPresent(); | ||||
|     } | ||||
| 
 | ||||
|     public static ProctoringWindowData getCurrentProctoringWindowData() { | ||||
|         return (ProctoringWindowData) RWT.getUISession() | ||||
|                 .getHttpSession() | ||||
|  | @ -105,7 +112,7 @@ public class ProctoringGUIService { | |||
|                 .withFormParam(ProctoringRoomConnection.ATTR_SUBJECT, subject) | ||||
|                 .call() | ||||
|                 .map(connection -> { | ||||
|                     this.openWindows.put(windowName, new RoomData(connection.roomName, examId)); | ||||
|                     this.openWindows.put(windowName, new RoomData(connection.roomName, examId, true)); | ||||
|                     return connection; | ||||
|                 }); | ||||
|     } | ||||
|  | @ -152,10 +159,16 @@ public class ProctoringGUIService { | |||
|     private static final class RoomData { | ||||
|         final String roomName; | ||||
|         final String examId; | ||||
|         final boolean isTownhall; | ||||
| 
 | ||||
|         public RoomData(final String roomName, final String examId) { | ||||
|             this(roomName, examId, false); | ||||
|         } | ||||
| 
 | ||||
|         public RoomData(final String roomName, final String examId, final boolean townhall) { | ||||
|             this.roomName = roomName; | ||||
|             this.examId = examId; | ||||
|             this.isTownhall = townhall; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -59,6 +59,8 @@ public interface RemoteProctoringRoomDAO { | |||
|      * @return Result refer to the created room record or to an error when happened */ | ||||
|     Result<RemoteProctoringRoom> createTownhallRoom(Long examId, NewRoom room); | ||||
| 
 | ||||
|     boolean isTownhallRoomActive(Long examId); | ||||
| 
 | ||||
|     /** Get the town hall room record for a given exam if existing. | ||||
|      * | ||||
|      * @param examId the exam identifier | ||||
|  |  | |||
|  | @ -20,6 +20,8 @@ import java.util.stream.Collectors; | |||
| 
 | ||||
| import org.apache.commons.lang3.BooleanUtils; | ||||
| import org.apache.commons.lang3.StringUtils; | ||||
| import org.slf4j.Logger; | ||||
| import org.slf4j.LoggerFactory; | ||||
| import org.springframework.context.annotation.Lazy; | ||||
| import org.springframework.stereotype.Component; | ||||
| import org.springframework.transaction.annotation.Transactional; | ||||
|  | @ -42,6 +44,8 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.proctoring.New | |||
| @WebServiceProfile | ||||
| public class RemoteProctoringRoomDAOImpl implements RemoteProctoringRoomDAO { | ||||
| 
 | ||||
|     private static final Logger log = LoggerFactory.getLogger(RemoteProctoringRoomDAOImpl.class); | ||||
| 
 | ||||
|     private static final Object RESERVE_ROOM_LOCK = new Object(); | ||||
| 
 | ||||
|     private final RemoteProctoringRoomRecordMapper remoteProctoringRoomRecordMapper; | ||||
|  | @ -129,15 +133,10 @@ public class RemoteProctoringRoomDAOImpl implements RemoteProctoringRoomDAO { | |||
|             final NewRoom room) { | ||||
| 
 | ||||
|         return Result.tryCatch(() -> { | ||||
|             // check first if town-hall room is not already active | ||||
|             final long active = this.remoteProctoringRoomRecordMapper.countByExample() | ||||
|                     .where(RemoteProctoringRoomRecordDynamicSqlSupport.examId, isEqualTo(examId)) | ||||
|                     .and(RemoteProctoringRoomRecordDynamicSqlSupport.townhallRoom, isNotEqualTo(0)) | ||||
|                     .build() | ||||
|                     .execute(); | ||||
| 
 | ||||
|             if (active > 0) { | ||||
|                 throw new IllegalStateException("Townhall, for exam: " + examId + " already existis"); | ||||
|             // Check first if town-hall room is not already active | ||||
|             if (isTownhallRoomActive(examId)) { | ||||
|                 throw new IllegalStateException("Townhall, for exam: " + examId + " already exists"); | ||||
|             } | ||||
| 
 | ||||
|             final RemoteProctoringRoomRecord townhallRoomRecord = new RemoteProctoringRoomRecord( | ||||
|  | @ -159,6 +158,24 @@ public class RemoteProctoringRoomDAOImpl implements RemoteProctoringRoomDAO { | |||
|                 .onError(TransactionHandler::rollback); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     @Transactional(readOnly = true) | ||||
|     public boolean isTownhallRoomActive(final Long examId) { | ||||
|         try { | ||||
|             final long active = this.remoteProctoringRoomRecordMapper.countByExample() | ||||
|                     .where(RemoteProctoringRoomRecordDynamicSqlSupport.examId, isEqualTo(examId)) | ||||
|                     .and(RemoteProctoringRoomRecordDynamicSqlSupport.townhallRoom, isNotEqualTo(0)) | ||||
|                     .build() | ||||
|                     .execute(); | ||||
|             return (active > 0); | ||||
|         } catch (final Exception e) { | ||||
|             log.error( | ||||
|                     "Failed to verify town-hall room activity for exam: {}. Mark it as active to avoid double openings", | ||||
|                     examId, e); | ||||
|             return true; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     @Transactional | ||||
|     public Result<EntityKey> deleteTownhallRoom(final Long examId) { | ||||
|  |  | |||
|  | @ -58,6 +58,8 @@ public interface ExamProctoringRoomService { | |||
|      * @return Result refer to the given exam or to an error when happened */ | ||||
|     Result<Exam> disposeRoomsForExam(Exam exam); | ||||
| 
 | ||||
|     boolean isTownhallRoomActive(final Long examId); | ||||
| 
 | ||||
|     /** This creates a town-hall room for a specific exam. The exam must be active and running | ||||
|      * and there must be no other town-hall room already be active. An unique room name will be | ||||
|      * created and returned. | ||||
|  |  | |||
|  | @ -318,6 +318,11 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public boolean isTownhallRoomActive(final Long examId) { | ||||
|         return this.remoteProctoringRoomDAO.isTownhallRoomActive(examId); | ||||
|     } | ||||
| 
 | ||||
|     private void closeTownhall( | ||||
|             final Long examId, | ||||
|             final ProctoringServiceSettings proctoringSettings, | ||||
|  | @ -593,4 +598,5 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService | |||
|                         true) | ||||
|                 .onError(error -> log.error("Failed to send join instruction: {}", connectionToken, error)); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -21,6 +21,7 @@ import javax.servlet.ServletOutputStream; | |||
| import javax.servlet.http.HttpServletRequest; | ||||
| import javax.servlet.http.HttpServletResponse; | ||||
| 
 | ||||
| import org.apache.commons.lang3.StringUtils; | ||||
| import org.slf4j.Logger; | ||||
| import org.slf4j.LoggerFactory; | ||||
| import org.springframework.beans.factory.annotation.Qualifier; | ||||
|  | @ -35,6 +36,7 @@ import org.springframework.web.bind.annotation.RequestParam; | |||
| import org.springframework.web.bind.annotation.ResponseStatus; | ||||
| 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.APIMessage; | ||||
| import ch.ethz.seb.sebserver.gbl.api.JSONMapper; | ||||
|  | @ -101,7 +103,7 @@ public class ExamAPI_V1_Controller { | |||
| 
 | ||||
|                     final POSTMapper mapper = new POSTMapper(formParams, request.getQueryString()); | ||||
| 
 | ||||
|                     final String remoteAddr = request.getRemoteAddr(); | ||||
|                     final String remoteAddr = this.getClientAddress(request); | ||||
|                     final Long institutionId = (instIdRequestParam != null) | ||||
|                             ? instIdRequestParam | ||||
|                             : mapper.getLong(API.PARAM_INSTITUTION_ID); | ||||
|  | @ -163,7 +165,7 @@ public class ExamAPI_V1_Controller { | |||
|         return CompletableFuture.runAsync( | ||||
|                 () -> { | ||||
| 
 | ||||
|                     final String remoteAddr = request.getRemoteAddr(); | ||||
|                     final String remoteAddr = this.getClientAddress(request); | ||||
|                     final Long institutionId = getInstitutionId(principal); | ||||
| 
 | ||||
|                     this.sebClientConnectionService.updateClientConnection( | ||||
|  | @ -193,7 +195,7 @@ public class ExamAPI_V1_Controller { | |||
|         return CompletableFuture.runAsync( | ||||
|                 () -> { | ||||
| 
 | ||||
|                     final String remoteAddr = request.getRemoteAddr(); | ||||
|                     final String remoteAddr = this.getClientAddress(request); | ||||
|                     final Long institutionId = getInstitutionId(principal); | ||||
| 
 | ||||
|                     this.sebClientConnectionService.establishClientConnection( | ||||
|  | @ -220,7 +222,7 @@ public class ExamAPI_V1_Controller { | |||
|         return CompletableFuture.runAsync( | ||||
|                 () -> { | ||||
| 
 | ||||
|                     final String remoteAddr = request.getRemoteAddr(); | ||||
|                     final String remoteAddr = this.getClientAddress(request); | ||||
|                     final Long institutionId = getInstitutionId(principal); | ||||
| 
 | ||||
|                     if (log.isDebugEnabled()) { | ||||
|  | @ -406,4 +408,23 @@ public class ExamAPI_V1_Controller { | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private String getClientAddress(final HttpServletRequest request) { | ||||
|         try { | ||||
|             final String ipAddress = request.getHeader("X-FORWARDED-FOR"); | ||||
| 
 | ||||
|             if (ipAddress == null) { | ||||
|                 return request.getRemoteAddr(); | ||||
|             } | ||||
| 
 | ||||
|             if (ipAddress.contains(",")) { | ||||
|                 return StringUtils.split(ipAddress, Constants.COMMA)[0]; | ||||
|             } | ||||
| 
 | ||||
|             return ipAddress; | ||||
|         } catch (final Exception e) { | ||||
|             log.warn("Failed to verify client IP address: {}", e.getMessage()); | ||||
|             return request.getHeader("X-FORWARDED-FOR"); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -234,6 +234,22 @@ public class ExamProctoringController { | |||
|                 .onError(error -> log.error("Failed to close remote proctoring break out room {}", roomName, error)); | ||||
|     } | ||||
| 
 | ||||
|     @RequestMapping( | ||||
|             path = API.MODEL_ID_VAR_PATH_SEGMENT | ||||
|                     + API.EXAM_PROCTORING_TOWNHALL_ROOM_AVAILABLE, | ||||
|             method = RequestMethod.GET, | ||||
|             produces = MediaType.APPLICATION_JSON_VALUE) | ||||
|     public String isTownhallRoomAvialbale( | ||||
|             @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) { | ||||
| 
 | ||||
|         checkExamReadAccess(institutionId); | ||||
|         return String.valueOf(!this.examProcotringRoomService.isTownhallRoomActive(examId)); | ||||
|     } | ||||
| 
 | ||||
|     @RequestMapping( | ||||
|             path = API.MODEL_ID_VAR_PATH_SEGMENT | ||||
|                     + API.EXAM_PROCTORING_TOWNHALL_ROOM_DATA, | ||||
|  | @ -246,7 +262,7 @@ public class ExamProctoringController { | |||
|                     defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId, | ||||
|             @PathVariable(name = API.PARAM_MODEL_ID) final Long examId) { | ||||
| 
 | ||||
|         checkAccess(institutionId, examId); | ||||
|         checkExamReadAccess(institutionId); | ||||
|         return this.examProcotringRoomService | ||||
|                 .getTownhallRoomData(examId) | ||||
|                 .getOrElse(() -> RemoteProctoringRoom.NULL_ROOM); | ||||
|  | @ -273,6 +289,13 @@ public class ExamProctoringController { | |||
|                 .getOrThrow(); | ||||
|     } | ||||
| 
 | ||||
|     private void checkExamReadAccess(final Long institutionId) { | ||||
|         this.authorization.check( | ||||
|                 PrivilegeType.READ, | ||||
|                 EntityType.EXAM, | ||||
|                 institutionId); | ||||
|     } | ||||
| 
 | ||||
|     private void checkAccess(final Long institutionId, final Long examId) { | ||||
|         this.authorization.check( | ||||
|                 PrivilegeType.READ, | ||||
|  |  | |||
		Loading…
	
	Add table
		
		Reference in a new issue
	
	 anhefti
						anhefti