diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/util/SizedArrayNonBlockingQueue.java b/src/main/java/ch/ethz/seb/sebserver/gbl/util/SizedArrayNonBlockingQueue.java new file mode 100644 index 00000000..c9df8e49 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/util/SizedArrayNonBlockingQueue.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2020 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package ch.ethz.seb.sebserver.gbl.util; + +import java.util.concurrent.ArrayBlockingQueue; + +public class SizedArrayNonBlockingQueue extends ArrayBlockingQueue { + + private static final long serialVersionUID = -4235702373708064610L; + + private final int size; + + public SizedArrayNonBlockingQueue(final int size) { + super(size); + this.size = size; + } + + @Override + synchronized public boolean add(final T element) { + // Check if queue full already? + if (super.size() == this.size) { + // remove element from queue if queue is full + this.remove(); + } + return super.add(element); + } + +} 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 f09f70c6..28ade6c5 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 @@ -334,26 +334,19 @@ public class MonitoringRunningExam implements TemplateComposer { if (proctoringSettings != null && proctoringSettings.enableProctoring) { - final RemoteProctoringRoom townhall = restService.getBuilder(GetTownhallRoom.class) - .withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId) - .call() - .getOr(null); - - final boolean townhallActive = townhall != null && townhall.id != null; actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM) .withEntityKey(entityKey) - .withExec(this::openTownhallRoom) + .withExec(this::toggleTownhallRoom) .noEventPropagation() .publish(); - actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_CLOSE_TOWNHALL_PROCTOR_ROOM) - .withEntityKey(entityKey) - .withExec(this::closeTownhallRoom) - .noEventPropagation() - .publish(); - if (!townhallActive) { + if (isTownhallRoomActive(entityKey.modelId)) { this.pageService.firePageEvent( - new ActionActivationEvent(false, ActionDefinition.MONITOR_EXAM_CLOSE_TOWNHALL_PROCTOR_ROOM), + new ActionActivationEvent( + true, + new Tuple<>( + ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM, + ActionDefinition.MONITOR_EXAM_CLOSE_TOWNHALL_PROCTOR_ROOM)), pageContext); } @@ -376,6 +369,40 @@ public class MonitoringRunningExam implements TemplateComposer { } } + private boolean isTownhallRoomActive(final String examModelId) { + final RemoteProctoringRoom townhall = this.pageService.getRestService() + .getBuilder(GetTownhallRoom.class) + .withURIVariable(API.PARAM_MODEL_ID, examModelId) + .call() + .getOr(null); + + return townhall != null && townhall.id != null; + } + + private PageAction toggleTownhallRoom(final PageAction action) { + if (isTownhallRoomActive(action.getEntityKey().modelId)) { + closeTownhallRoom(action); + this.pageService.firePageEvent( + new ActionActivationEvent( + true, + new Tuple<>( + ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM, + ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM)), + action.pageContext()); + return action; + } else { + openTownhallRoom(action); + this.pageService.firePageEvent( + new ActionActivationEvent( + true, + new Tuple<>( + ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM, + ActionDefinition.MONITOR_EXAM_CLOSE_TOWNHALL_PROCTOR_ROOM)), + action.pageContext()); + return action; + } + } + private PageAction openTownhallRoom(final PageAction action) { final EntityKey examId = action.getEntityKey(); @@ -407,11 +434,6 @@ public class MonitoringRunningExam implements TemplateComposer { this.remoteProctoringEndpoint); javaScriptExecutor.execute(script); proctoringGUIService.registerProctoringWindow(activeAllRoomName); - this.pageService.firePageEvent( - new ActionActivationEvent( - true, - ActionDefinition.MONITOR_EXAM_CLOSE_TOWNHALL_PROCTOR_ROOM), - action.pageContext()); return action; } @@ -431,14 +453,29 @@ public class MonitoringRunningExam implements TemplateComposer { .getProctoringGUIService(); proctoringGUIService.closeRoom(townhall.name); - this.pageService.firePageEvent( - new ActionActivationEvent( - false, - ActionDefinition.MONITOR_EXAM_CLOSE_TOWNHALL_PROCTOR_ROOM), - action.pageContext()); return action; } + private void updateTownhallButton(final EntityKey entityKey, final PageContext pageContext) { + 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); + } else { + this.pageService.firePageEvent( + new ActionActivationEvent( + true, + new Tuple<>( + ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM, + ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM)), + pageContext); + } + } + private void updateRoomActions( final EntityKey entityKey, final PageContext pageContext, @@ -446,6 +483,7 @@ public class MonitoringRunningExam implements TemplateComposer { final PageActionBuilder actionBuilder, final ProctoringSettings proctoringSettings) { + updateTownhallButton(entityKey, pageContext); final I18nSupport i18nSupport = this.pageService.getI18nSupport(); this.pageService.getRestService().getBuilder(GetProcotringRooms.class) .withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId) diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionDefinition.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionDefinition.java index 26786b6f..f3578601 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionDefinition.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionDefinition.java @@ -711,7 +711,7 @@ public enum ActionDefinition { ActionCategory.PROCTORING), MONITOR_EXAM_CLOSE_TOWNHALL_PROCTOR_ROOM( new LocTextKey("sebserver.monitoring.exam.action.proctoring.closeTownhall"), - ImageIcon.PROCTOR_ROOM, + ImageIcon.CANCEL, PageStateDefinitionImpl.MONITORING_RUNNING_EXAM, ActionCategory.PROCTORING), diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionPane.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionPane.java index bd2ac5a0..7f911a69 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionPane.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionPane.java @@ -135,13 +135,11 @@ public class ActionPane implements TemplateComposer { if (event.decoration != null) { final TreeItem actionItemToDecorate = findAction(actionTrees, parent, event.decoration._1); - final PageAction action = (PageAction) actionItemToDecorate.getData(ACTION_EVENT_CALL_KEY); if (actionItemToDecorate != null && event.decoration._2 != null) { actionItemToDecorate.setImage(0, event.decoration._2.icon.getImage(parent.getDisplay())); ActionPane.this.pageService.getPolyglotPageService().injectI18n( - actionItemToDecorate, - (action != null) ? action.getTitle() : event.decoration._2.title); + actionItemToDecorate, event.decoration._2.title); } } }); 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 b7b8e2d3..aa9ff9a9 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 @@ -29,12 +29,10 @@ import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings.ProctoringServerT import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction; import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; import ch.ethz.seb.sebserver.gui.GuiServiceInfo; -import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition; import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; import ch.ethz.seb.sebserver.gui.service.page.PageContext; import ch.ethz.seb.sebserver.gui.service.page.PageService; import ch.ethz.seb.sebserver.gui.service.page.RemoteProctoringView; -import ch.ethz.seb.sebserver.gui.service.page.event.ActionActivationEvent; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.SendProctoringBroadcastAttributes; import ch.ethz.seb.sebserver.gui.service.session.ProctoringGUIService; import ch.ethz.seb.sebserver.gui.service.session.ProctoringGUIService.ProctoringWindowData; @@ -92,7 +90,7 @@ public class JitsiMeetProctoringView implements RemoteProctoringView { final GridData headerCell = new GridData(SWT.FILL, SWT.FILL, true, true); content.setLayoutData(headerCell); - parent.addListener(SWT.Dispose, event -> closeRoom(proctoringWindowData, pageContext)); + parent.addListener(SWT.Dispose, event -> closeRoom(proctoringWindowData)); final String url = this.guiServiceInfo .getExternalServerURIBuilder() @@ -122,7 +120,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, pageContext)); + closeAction.addListener(SWT.Selection, event -> closeRoom(proctoringWindowData)); final BroadcastActionState broadcastActionState = new BroadcastActionState(); final String connectionTokens = getConnectionTokens(proctoringWindowData); @@ -258,16 +256,11 @@ public class JitsiMeetProctoringView implements RemoteProctoringView { boolean chat = false; } - private void closeRoom(final ProctoringWindowData proctoringWindowData, final PageContext pageContext) { + private void closeRoom(final ProctoringWindowData proctoringWindowData) { this.pageService .getCurrentUser() .getProctoringGUIService() .closeRoom(proctoringWindowData.connectionData.roomName); - this.pageService.firePageEvent( - new ActionActivationEvent( - false, - ActionDefinition.MONITOR_EXAM_CLOSE_TOWNHALL_PROCTOR_ROOM), - pageContext); } } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/session/ProctoringGUIService.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/ProctoringGUIService.java index 8f5e6a7b..c42c1bec 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/session/ProctoringGUIService.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/ProctoringGUIService.java @@ -199,6 +199,11 @@ public class ProctoringGUIService { this.restService.getBuilder(SendProctoringBroadcastAttributes.class) .withURIVariable(API.PARAM_MODEL_ID, roomConnectionData.examId) .withFormParam(Domain.REMOTE_PROCTORING_ROOM.ATTR_ID, roomConnectionData.roomName) + .withFormParam( + API.EXAM_API_SEB_CONNECTION_TOKEN, + roomConnectionData.connections.isEmpty() + ? "" + : StringUtils.join(roomConnectionData.connections, Constants.LIST_SEPARATOR_CHAR)) .call() .onError(error -> log.error( "Failed to send reset broadcast attribute instruction call for room: {}, cause: {}", @@ -229,7 +234,9 @@ public class ProctoringGUIService { public void clear() { if (!this.rooms.isEmpty()) { - this.rooms.keySet().stream().forEach(this::closeRoom); + this.rooms.keySet() + .stream() + .forEach(this::closeRoom); this.rooms.clear(); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBInstructionServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBInstructionServiceImpl.java index 43b4af17..f1d284fe 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBInstructionServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBInstructionServiceImpl.java @@ -16,8 +16,6 @@ import java.util.concurrent.ConcurrentHashMap; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; -import org.joda.time.DateTime; -import org.joda.time.DateTimeZone; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Lazy; @@ -31,6 +29,7 @@ import ch.ethz.seb.sebserver.gbl.api.JSONMapper; import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction.InstructionType; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.gbl.util.Result; +import ch.ethz.seb.sebserver.gbl.util.SizedArrayNonBlockingQueue; import ch.ethz.seb.sebserver.gbl.util.Utils; import ch.ethz.seb.sebserver.webservice.WebserviceInfo; import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientInstructionRecord; @@ -45,6 +44,7 @@ public class SEBInstructionServiceImpl implements SEBInstructionService { private static final Logger log = LoggerFactory.getLogger(SEBInstructionServiceImpl.class); + private static final int INSTRUCTION_QUEUE_MAX_SIZE = 10; private static final String JSON_INST = "instruction"; private static final String JSON_ATTR = "attributes"; @@ -53,7 +53,7 @@ public class SEBInstructionServiceImpl implements SEBInstructionService { private final ClientInstructionDAO clientInstructionDAO; private final JSONMapper jsonMapper; - private final Map instructions; + private final Map> instructions; private long lastRefresh = 0; @@ -113,7 +113,7 @@ public class SEBInstructionServiceImpl implements SEBInstructionService { final String attributesString = this.jsonMapper.writeValueAsString(attributes); this.clientInstructionDAO .insert(examId, type, attributesString, connectionToken, needsConfirm) - .map(this::chacheInstruction) + .map(this::putToCacheIfAbsent) .onError(error -> log.error("Failed to register instruction: {}", error.getMessage())) .getOrThrow(); } catch (final Exception e) { @@ -146,7 +146,7 @@ public class SEBInstructionServiceImpl implements SEBInstructionService { error -> log.error("Failed to register instruction: {}", error.getMessage()), () -> null)) .filter(Objects::nonNull) - .forEach(this::chacheInstruction); + .forEach(this::putToCacheIfAbsent); }); } @@ -162,10 +162,19 @@ public class SEBInstructionServiceImpl implements SEBInstructionService { return null; } - final ClientInstructionRecord clientInstruction = this.instructions.get(connectionToken); + final SizedArrayNonBlockingQueue queue = this.instructions.get(connectionToken); + if (queue.isEmpty()) { + return null; + } + + final ClientInstructionRecord clientInstruction = queue.peek(); + if (clientInstruction == null) { + return null; + } + final boolean needsConfirm = BooleanUtils.toBoolean(clientInstruction.getNeedsConfirmation()); if (!needsConfirm) { - this.instructions.remove(connectionToken); + queue.poll(); final Result delete = this.clientInstructionDAO.delete(clientInstruction.getId()); if (delete.hasError()) { log.error("Failed to delete SEB client instruction on persistent storage: ", delete.getError()); @@ -207,13 +216,26 @@ public class SEBInstructionServiceImpl implements SEBInstructionService { @Override public void confirmInstructionDone(final String connectionToken, final String instructionConfirm) { try { - this.instructions.remove(connectionToken); - this.clientInstructionDAO.delete(Long.valueOf(instructionConfirm)); + final SizedArrayNonBlockingQueue queue = this.instructions.get(connectionToken); + if (queue.isEmpty()) { + return; + } + + final ClientInstructionRecord instruction = queue.peek(); + if (String.valueOf(instruction.getId()).equals(String.valueOf(instruction.getId()))) { + queue.poll(); + this.clientInstructionDAO.delete(Long.valueOf(instructionConfirm)); + } else { + log.warn("SEB instruction confirmation mismatch. Sent instructionConfirm: {} pending instruction: {}", + instructionConfirm, + instruction.getId()); + } } catch (final Exception e) { log.error( - "Failed to remove SEB instruction after confirmation: connectionToken: {} instructionConfirm: {}", + "Failed to remove SEB instruction after confirmation: connectionToken: {} instructionConfirm: {} connectionToken: {}", connectionToken, - instructionConfirm); + instructionConfirm, + connectionToken); } } @@ -236,33 +258,53 @@ public class SEBInstructionServiceImpl implements SEBInstructionService { private Result loadInstructions() { return Result.tryCatch(() -> this.clientInstructionDAO.getAllActive() .getOrThrow() - .forEach(inst -> this.instructions.putIfAbsent(inst.getConnectionToken(), inst))); + .forEach(this::putToCacheIfAbsent)); } - private ClientInstructionRecord chacheInstruction(final ClientInstructionRecord instruction) { +// private ClientInstructionRecord chacheInstruction(final ClientInstructionRecord instruction) { +// +// +// +// final String connectionToken = instruction.getConnectionToken(); +// if (this.instructions.containsKey(connectionToken)) { +// // check if previous instruction is still valid +// final ClientInstructionRecord clientInstructionRecord = this.instructions.get(connectionToken); +// +// System.out.println("************* previous instruction still active: " + clientInstructionRecord); +// +// if (BooleanUtils.toBoolean(BooleanUtils.toBooleanObject(clientInstructionRecord.getNeedsConfirmation()))) { +// // check if time is out +// final long now = DateTime.now(DateTimeZone.UTC).getMillis(); +// final Long timestamp = clientInstructionRecord.getTimestamp(); +// if (timestamp != null && now - timestamp > Constants.MINUTE_IN_MILLIS) { +// // remove old instruction and add new one +// System.out.println("************* remove old instruction and put new: "); +// this.instructions.put(connectionToken, instruction); +// } +// } +// } else { +// this.instructions.put(connectionToken, instruction); +// } +// return instruction; +// } + + private ClientInstructionRecord putToCacheIfAbsent(final ClientInstructionRecord instruction) { + final SizedArrayNonBlockingQueue queue = this.instructions.computeIfAbsent( + instruction.getConnectionToken(), + key -> new SizedArrayNonBlockingQueue<>(INSTRUCTION_QUEUE_MAX_SIZE)); + + if (queue.contains(instruction)) { + log.warn("Instruction alread in the queue: {}", instruction); + return instruction; + } + + if (log.isDebugEnabled()) { + log.debug("Put SEB instruction into instruction queue: {}", instruction); + } System.out.println("************* register instruction: " + instruction); - final String connectionToken = instruction.getConnectionToken(); - if (this.instructions.containsKey(connectionToken)) { - // check if previous instruction is still valid - final ClientInstructionRecord clientInstructionRecord = this.instructions.get(connectionToken); - - System.out.println("************* previous instruction still active: " + clientInstructionRecord); - - if (BooleanUtils.toBoolean(BooleanUtils.toBooleanObject(clientInstructionRecord.getNeedsConfirmation()))) { - // check if time is out - final long now = DateTime.now(DateTimeZone.UTC).getMillis(); - final Long timestamp = clientInstructionRecord.getTimestamp(); - if (timestamp != null && now - timestamp > Constants.MINUTE_IN_MILLIS) { - // remove old instruction and add new one - System.out.println("************* remove old instruction and put new: "); - this.instructions.put(connectionToken, instruction); - } - } - } else { - this.instructions.put(connectionToken, instruction); - } + queue.add(instruction); return instruction; } diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index f009b5c6..9c1806ef 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -1,4 +1,3 @@ -################################ # Overall ################################