SEBSERV-139 refactoring of townhall and instruction service
This commit is contained in:
parent
8e04e43bfa
commit
53bb378d0b
8 changed files with 184 additions and 73 deletions
|
@ -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<T> extends ArrayBlockingQueue<T> {
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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),
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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<String, ClientInstructionRecord> instructions;
|
||||
private final Map<String, SizedArrayNonBlockingQueue<ClientInstructionRecord>> 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<ClientInstructionRecord> 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<Void> 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<ClientInstructionRecord> 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<Void> 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<ClientInstructionRecord> 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;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
################################
|
||||
# Overall
|
||||
################################
|
||||
|
||||
|
|
Loading…
Reference in a new issue