Merge remote-tracking branch 'origin/rel-1.2.4'

This commit is contained in:
anhefti 2021-11-10 14:39:04 +01:00
commit ee7a57fae9
47 changed files with 1222 additions and 160 deletions

View file

@ -18,7 +18,7 @@
<packaging>jar</packaging>
<properties>
<sebserver-version>1.2.3</sebserver-version>
<sebserver-version>1.2.4</sebserver-version>
<build-version>${sebserver-version}</build-version>
<revision>${sebserver-version}</revision>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

View file

@ -24,6 +24,8 @@ import ch.ethz.seb.sebserver.gbl.api.authorization.Privilege;
/** Global Constants used in SEB Server web-service as well as in web-gui component */
public final class Constants {
public static final String FILE_EXT_CSV = ".csv";
public static final String DEFAULT_LANG_CODE = "en";
public static final String DEFAULT_TIME_ZONE_CODE = "UTC";
public static final String TOOLTIP_TEXT_KEY_SUFFIX = ".tooltip";

View file

@ -197,6 +197,10 @@ public final class API {
public static final String SEB_CLIENT_EVENT_ENDPOINT = "/seb-client-event";
public static final String SEB_CLIENT_EVENT_SEARCH_PATH_SEGMENT = "/search";
public static final String SEB_CLIENT_EVENT_EXPORT_PATH_SEGMENT = "/export";
public static final String SEB_CLIENT_EVENT_EXPORT_TYPE = "exportType";
public static final String SEB_CLIENT_EVENT_EXPORT_INCLUDE_CONNECTIONS = "includeConnectionDetails";
public static final String SEB_CLIENT_EVENT_EXPORT_INCLUDE_EXAMS = "includeExamDetails";
public static final String SEB_CLIENT_EVENT_EXTENDED_PAGE_ENDPOINT = SEB_CLIENT_EVENT_ENDPOINT
+ SEB_CLIENT_EVENT_SEARCH_PATH_SEGMENT;

View file

@ -128,9 +128,9 @@ public final class Indicator implements Entity {
this.thresholds = Utils.immutableListOf(thresholds);
}
public Indicator(final Exam exam, final POSTMapper postParams) {
public Indicator(final Long examId, final POSTMapper postParams) {
this.id = null;
this.examId = exam.id;
this.examId = examId;
this.name = postParams.getString(Domain.INDICATOR.ATTR_NAME);
this.type = postParams.getEnum(Domain.INDICATOR.ATTR_TYPE, IndicatorType.class);
this.defaultColor = postParams.getString(Domain.INDICATOR.ATTR_COLOR);

View file

@ -68,6 +68,10 @@ public class ClientEvent implements Entity, IndicatorValueHolder {
}
}
public enum ExportType {
CSV
}
@JsonProperty(Domain.CLIENT_EVENT.ATTR_ID)
public final Long id;

View file

@ -262,6 +262,13 @@ public final class Result<T> {
}
}
public Result<T> onSuccess(final Consumer<T> handler) {
if (this.error == null) {
handler.accept(this.value);
}
return this;
}
/** Uses a given error handler to apply an error if there is one and returning itself again
* for further processing.
*

View file

@ -666,4 +666,19 @@ public final class Utils {
}
}
public static CharSequence trim(final CharSequence sequence) {
if (sequence == null) {
return null;
}
return StringUtils.trim(sequence.toString());
}
public static String toCSVString(final String text) {
if (StringUtils.isBlank(text)) {
return StringUtils.EMPTY;
}
return Constants.DOUBLE_QUOTE + text.replace("\"", "\"\"") + Constants.DOUBLE_QUOTE;
}
}

View file

@ -75,23 +75,25 @@ public class ProctorRoomConnectionsPopup {
.call()
.getOrThrow());
this.pageService.staticListTableBuilder(connections, EntityType.CLIENT_CONNECTION)
final EntityTable<ClientConnection> compose =
this.pageService.staticListTableBuilder(connections, EntityType.CLIENT_CONNECTION)
.withEmptyMessage(EMPTY_LIST_TEXT_KEY)
.withPaging(10)
.withEmptyMessage(EMPTY_LIST_TEXT_KEY)
.withPaging(10)
.withColumn(new ColumnDefinition<>(
Domain.CLIENT_CONNECTION.ATTR_EXAM_USER_SESSION_ID,
TABLE_COLUMN_NAME,
ClientConnection::getUserSessionId))
.withColumn(new ColumnDefinition<>(
Domain.CLIENT_CONNECTION.ATTR_EXAM_USER_SESSION_ID,
TABLE_COLUMN_NAME,
ClientConnection::getUserSessionId))
.withDefaultAction(t -> actionBuilder
.newAction(ActionDefinition.MONITOR_EXAM_CLIENT_CONNECTION)
.withParentEntityKey(parentEntityKey)
.withExec(action -> showClientConnection(action, dialog, t))
.create())
.withDefaultAction(t -> actionBuilder
.newAction(ActionDefinition.MONITOR_EXAM_CLIENT_CONNECTION)
.withParentEntityKey(parentEntityKey)
.withExec(action -> showClientConnection(action, dialog, t))
.create())
.compose(pageContext);
.compose(pageContext);
compose.reset();
}
private PageAction showClientConnection(

View file

@ -8,6 +8,7 @@
package ch.ethz.seb.sebserver.gui.content;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
@ -15,6 +16,8 @@ import java.util.function.Function;
import java.util.stream.Collectors;
import org.apache.tomcat.util.buf.StringUtils;
import org.eclipse.rap.rwt.RWT;
import org.eclipse.rap.rwt.client.service.UrlLauncher;
import org.eclipse.swt.widgets.Composite;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -24,12 +27,15 @@ import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.api.authorization.PrivilegeType;
import ch.ethz.seb.sebserver.gbl.model.Domain;
import ch.ethz.seb.sebserver.gbl.model.EntityName;
import ch.ethz.seb.sebserver.gbl.model.Page;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.ExportType;
import ch.ethz.seb.sebserver.gbl.model.session.ExtendedClientEvent;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gbl.util.Utils;
@ -42,6 +48,8 @@ import ch.ethz.seb.sebserver.gui.service.page.PageService;
import ch.ethz.seb.sebserver.gui.service.page.PageService.PageActionBuilder;
import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer;
import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction;
import ch.ethz.seb.sebserver.gui.service.remote.download.DownloadService;
import ch.ethz.seb.sebserver.gui.service.remote.download.SEBClientLogExport;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.logs.GetClientEventNames;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.logs.GetExtendedClientEventPage;
@ -84,23 +92,29 @@ public class SEBClientEvents implements TemplateComposer {
private final ResourceService resourceService;
private final RestService restService;
private final I18nSupport i18nSupport;
private final DownloadService downloadService;
private final SEBClientEventDetailsPopup sebClientEventDetailsPopup;
private final SEBClientEventDeletePopup sebClientEventDeletePopup;
private final int pageSize;
private final String exportFileName;
public SEBClientEvents(
final PageService pageService,
final DownloadService downloadService,
final SEBClientEventDetailsPopup sebClientEventDetailsPopup,
final SEBClientEventDeletePopup sebClientEventDeletePopup,
@Value("${sebserver.gui.seb.client.logs.export.filename:SEBClientLogs}") final String exportFileName,
@Value("${sebserver.gui.list.page.size:20}") final Integer pageSize) {
this.pageService = pageService;
this.downloadService = downloadService;
this.resourceService = pageService.getResourceService();
this.restService = this.resourceService.getRestService();
this.i18nSupport = this.resourceService.getI18nSupport();
this.sebClientEventDetailsPopup = sebClientEventDetailsPopup;
this.sebClientEventDeletePopup = sebClientEventDeletePopup;
this.pageSize = pageSize;
this.exportFileName = exportFileName;
this.examFilter = new TableFilterAttribute(
CriteriaType.SINGLE_SELECTION,
@ -219,12 +233,52 @@ public class SEBClientEvents implements TemplateComposer {
.noEventPropagation()
.publish(false)
.newAction(ActionDefinition.LOGS_SEB_CLIENT_EXPORT_CSV)
.withExec(action -> this.exportLogs(action, ExportType.CSV, table))
.noEventPropagation()
.publishIf(() -> writeGrant, table.hasAnyContent())
.newAction(ActionDefinition.LOGS_SEB_CLIENT_DELETE_ALL)
.withExec(action -> this.getOpenDelete(action, table.getFilterCriteria()))
.noEventPropagation()
.publishIf(() -> writeGrant, table.hasAnyContent());
}
private PageAction exportLogs(
final PageAction action,
final ExportType type,
final EntityTable<ExtendedClientEvent> table) {
try {
final UrlLauncher urlLauncher = RWT.getClient().getService(UrlLauncher.class);
final String fileName = this.exportFileName
+ Constants.UNDERLINE
+ this.i18nSupport.formatDisplayDate(Utils.getMillisecondsNow())
.replace(" ", "_")
.replace(".", "_")
+ Constants.FILE_EXT_CSV;
final Map<String, String> queryAttrs = new HashMap<>();
queryAttrs.put(API.SEB_CLIENT_EVENT_EXPORT_TYPE, type.name());
final String sortAttr = table.getSortOrder().encode(table.getSortColumn());
queryAttrs.put(Page.ATTR_SORT, sortAttr);
table.getFilterCriteria().forEach((name, value) -> queryAttrs.put(name, value.get(0)));
final String downloadURL = this.downloadService
.createDownloadURL(
SEBClientLogExport.class,
fileName,
queryAttrs);
urlLauncher.openURL(downloadURL);
} catch (final Exception e) {
log.error("Failed open export log download: ", e);
}
return action;
}
private PageAction getOpenDelete(
final PageAction pageAction,
final MultiValueMap<String, String> filterCriteria) {

View file

@ -770,6 +770,10 @@ public enum ActionDefinition {
ImageIcon.DELETE,
PageStateDefinitionImpl.SEB_CLIENT_LOGS,
ActionCategory.LOGS_SEB_CLIENT_LIST),
LOGS_SEB_CLIENT_EXPORT_CSV(
new LocTextKey("sebserver.seblogs.action.export.csv"),
ImageIcon.EXPORT,
ActionCategory.LOGS_SEB_CLIENT_LIST),
;

View file

@ -40,17 +40,8 @@ public abstract class AbstractDownloadServiceHandler implements DownloadServiceH
log.debug("download requested... trying to get needed parameter from request");
final String modelId = request.getParameter(API.PARAM_MODEL_ID);
if (StringUtils.isBlank(modelId)) {
log.error(
"Mandatory modelId parameter not found within HttpServletRequest. Download request is ignored");
return;
}
if (log.isDebugEnabled()) {
log.debug(
"Found modelId: {} for {} download. Trying to request webservice...",
modelId,
downloadFileName);
log.debug("Found modelId: {} for {} download.", modelId);
}
final String parentModelId = request.getParameter(API.PARAM_PARENT_MODEL_ID);
@ -67,7 +58,7 @@ public abstract class AbstractDownloadServiceHandler implements DownloadServiceH
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, header);
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
webserviceCall(modelId, parentModelId, response.getOutputStream());
webserviceCall(modelId, parentModelId, response.getOutputStream(), request);
} catch (final Exception e) {
log.error(
@ -76,6 +67,10 @@ public abstract class AbstractDownloadServiceHandler implements DownloadServiceH
}
}
protected abstract void webserviceCall(String modelId, String parentModelId, OutputStream downloadOut);
protected abstract void webserviceCall(
String modelId,
String parentModelId,
OutputStream downloadOut,
HttpServletRequest request);
}

View file

@ -8,9 +8,14 @@
package ch.ethz.seb.sebserver.gui.service.remote.download;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import java.util.Collection;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.rap.rwt.RWT;
import org.eclipse.rap.rwt.service.ServiceHandler;
@ -19,12 +24,10 @@ import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Collection;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gbl.util.Utils;
/** Implements a eclipse RAP ServiceHandler to handle downloads */
@Lazy
@ -73,6 +76,33 @@ public class DownloadService implements ServiceHandler {
.processDownload(request, response);
}
public String createDownloadURL(
final Class<? extends DownloadServiceHandler> handlerClass,
final String downloadFileName,
final Map<String, String> queryAttrs) {
final StringBuilder url = new StringBuilder()
.append(RWT.getServiceManager()
.getServiceHandlerUrl(DownloadService.DOWNLOAD_SERVICE_NAME))
.append(Constants.FORM_URL_ENCODED_SEPARATOR)
.append(DownloadService.HANDLER_NAME_PARAMETER)
.append(Constants.FORM_URL_ENCODED_NAME_VALUE_SEPARATOR)
.append(handlerClass.getSimpleName())
.append(Constants.FORM_URL_ENCODED_SEPARATOR)
.append(DownloadService.DOWNLOAD_FILE_NAME)
.append(Constants.FORM_URL_ENCODED_NAME_VALUE_SEPARATOR)
.append(downloadFileName);
queryAttrs.forEach((name, value) -> {
url.append(Constants.FORM_URL_ENCODED_SEPARATOR)
.append(name)
.append(Constants.FORM_URL_ENCODED_NAME_VALUE_SEPARATOR)
.append(Utils.encodeFormURL_UTF_8(value));
});
return url.toString();
}
public String createDownloadURL(
final String modelId,
final Class<? extends DownloadServiceHandler> handlerClass,

View file

@ -12,6 +12,8 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
@ -35,14 +37,16 @@ public class SEBClientConfigDownload extends AbstractDownloadServiceHandler {
private final RestService restService;
protected SEBClientConfigDownload(
final RestService restService) {
protected SEBClientConfigDownload(final RestService restService) {
this.restService = restService;
}
@Override
protected void webserviceCall(final String modelId, final String parentModelId, final OutputStream downloadOut) {
protected void webserviceCall(
final String modelId,
final String parentModelId,
final OutputStream downloadOut,
final HttpServletRequest request) {
final RestCall<InputStream>.RestCallBuilder restCallBuilder = this.restService
.getBuilder(ExportClientConfig.class)

View file

@ -0,0 +1,80 @@
/*
* 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.download;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Enumeration;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.ExportSEBClientLogs;
@Lazy
@Component
@GuiProfile
public class SEBClientLogExport extends AbstractDownloadServiceHandler {
private static final Logger log = LoggerFactory.getLogger(SEBClientLogExport.class);
private final RestService restService;
protected SEBClientLogExport(final RestService restService) {
this.restService = restService;
}
@Override
protected void webserviceCall(
final String modelId,
final String parentModelId,
final OutputStream downloadOut,
final HttpServletRequest request) {
final MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
final Enumeration<String> paramNames = request.getParameterNames();
while (paramNames.hasMoreElements()) {
final String param = paramNames.nextElement();
queryParams.add(param, String.valueOf(request.getParameter(param)));
}
final InputStream input = this.restService
.getBuilder(ExportSEBClientLogs.class)
.withQueryParams(queryParams)
.call()
.getOrThrow();
try {
IOUtils.copyLarge(input, downloadOut);
} catch (final IOException e) {
log.error(
"Unexpected error while streaming incoming config data from web-service to output-stream of download response: ",
e);
} finally {
try {
downloadOut.flush();
downloadOut.close();
} catch (final IOException e) {
log.error("Unexpected error while trying to close download output-stream");
}
}
}
}

View file

@ -12,6 +12,8 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -37,7 +39,11 @@ public class SEBExamConfigDownload extends AbstractDownloadServiceHandler {
}
@Override
protected void webserviceCall(final String modelId, final String parentModelId, final OutputStream downloadOut) {
protected void webserviceCall(
final String modelId,
final String parentModelId,
final OutputStream downloadOut,
final HttpServletRequest request) {
final InputStream input = this.restService.getBuilder(ExportExamConfig.class)
.withURIVariable(API.PARAM_MODEL_ID, modelId)

View file

@ -12,6 +12,8 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -37,7 +39,11 @@ public class SEBExamConfigPlaintextDownload extends AbstractDownloadServiceHandl
}
@Override
protected void webserviceCall(final String modelId, final String parentModelId, final OutputStream downloadOut) {
protected void webserviceCall(
final String modelId,
final String parentModelId,
final OutputStream downloadOut,
final HttpServletRequest request) {
final InputStream input = this.restService.getBuilder(ExportPlainXML.class)
.withURIVariable(API.PARAM_MODEL_ID, modelId)

View file

@ -0,0 +1,42 @@
/*
* 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.exam;
import java.io.InputStream;
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.AbstractExportCall;
@Lazy
@Component
@GuiProfile
public class ExportSEBClientLogs extends AbstractExportCall {
public ExportSEBClientLogs() {
super(new TypeKey<>(
CallType.UNDEFINED,
EntityType.CLIENT_EVENT,
new TypeReference<InputStream>() {
}),
HttpMethod.GET,
MediaType.APPLICATION_FORM_URLENCODED,
API.SEB_CLIENT_EVENT_ENDPOINT
+ API.SEB_CLIENT_EVENT_EXPORT_PATH_SEGMENT);
}
}

View file

@ -243,6 +243,14 @@ public class EntityTable<ROW> {
return this.name;
}
public String getSortColumn() {
return this.sortColumn;
}
public PageSortOrder getSortOrder() {
return this.sortOrder;
}
public EntityType getEntityType() {
if (this.pageSupplier != null) {
return this.pageSupplier.getEntityType();

View file

@ -106,8 +106,17 @@ public class StaticListPageSupplier<T> implements PageSupplier<T> {
if (numOfPages <= 0) {
return new Page<>(1, 1, this.column, this.list);
}
final List<T> subList = this.list.subList(this.pageNumber * this.pageSize,
this.pageNumber * this.pageSize + this.pageSize);
int from = (this.pageNumber - 1) * this.pageSize;
if (from < 0) {
from = 0;
}
int to = (this.pageNumber - 1) * this.pageSize + this.pageSize;
if (to >= this.list.size()) {
to = this.list.size();
}
final List<T> subList = this.list.subList(from, to);
return new Page<>(numOfPages, this.pageNumber, this.column, subList);
});
}

View file

@ -77,6 +77,30 @@ public interface PaginationService {
final String tableName,
final Supplier<Result<Collection<T>>> delegate);
/** Fetches a paged batch of objects
*
* NOTE: Paging always depends on SQL level. It depends on the collection given by the SQL select statement
* that is executed within MyBatis by using the MyBatis page service.
* Be aware that if the delegate that is given here applies an additional filter to the filtering done
* on SQL level, this will lead to paging with not fully filled pages or even to empty pages if the filter
* filters a lot of the entries given by the SQL statement away.
* So we recommend to apply as much of the filtering as possible on the SQL level and only if necessary and
* not avoidable, apply a additional filter on software-level that eventually filter one or two entities
* for a page.
*
* @param pageNumber the current page number
* @param pageSize the (full) size of the page
* @param sort the name of the sort column with a leading '-' for descending sort order
* @param tableName the name of the SQL table on which the pagination is applying to
* @param delegate a collection supplier the does the underling SQL query with specified pagination attributes
* @return Result refers to a Collection of specified type of objects or to an exception on error case */
<T> Result<Page<T>> getPageOf(
final Integer pageNumber,
final Integer pageSize,
final String sort,
final String tableName,
final Supplier<Result<Collection<T>>> delegate);
/** Use this to build a current Page from a given list of objects.
*
* @param <T> the Type if list entities

View file

@ -147,6 +147,29 @@ public class PaginationServiceImpl implements PaginationService {
});
}
@Override
public <T> Result<Page<T>> getPageOf(
final Integer pageNumber,
final Integer pageSize,
final String sort,
final String tableName,
final Supplier<Result<Collection<T>>> delegate) {
return Result.tryCatch(() -> {
//final SqlTable table = SqlTable.of(tableName);
final com.github.pagehelper.Page<Object> page =
setPagination(pageNumber, pageSize, sort, tableName);
final Collection<T> list = delegate.get().getOrThrow();
return new Page<>(
page.getPages(),
page.getPageNum(),
sort,
list);
});
}
private String verifySortColumnName(final String sort, final String columnName) {
if (StringUtils.isBlank(sort)) {

View file

@ -132,6 +132,8 @@ public interface ClientConnectionDAO extends
key = "#connectionToken")
Result<Void> removeFromProctoringRoom(Long connectionId, String connectionToken);
Result<Void> markForProctoringUpdate(Long id);
/** Deletes the given ClientConnection data.
*
* This evicts all entries from the CONNECTION_TOKENS_CACHE.

View file

@ -389,6 +389,19 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO {
});
}
@Override
@Transactional
public Result<Void> markForProctoringUpdate(final Long id) {
return Result.tryCatch(() -> {
this.clientConnectionRecordMapper.updateByPrimaryKeySelective(new ClientConnectionRecord(
id,
null, null, null, null, null,
null, null, null, null, null, null,
null,
1));
});
}
@Override
@Transactional
public Result<Void> removeFromProctoringRoom(final Long connectionId, final String connectionToken) {

View file

@ -0,0 +1,37 @@
/*
* 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.webservice.servicelayer.exam;
import java.io.OutputStream;
import java.util.Collection;
import org.springframework.scheduling.annotation.Async;
import ch.ethz.seb.sebserver.gbl.async.AsyncServiceSpringConfig;
import ch.ethz.seb.sebserver.gbl.model.EntityProcessingReport;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.ExportType;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.impl.SEBServerUser;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
public interface SEBClientEventAdminService {
Result<EntityProcessingReport> deleteAllClientEvents(Collection<String> ids);
@Async(AsyncServiceSpringConfig.EXECUTOR_BEAN_NAME)
void exportSEBClientLogs(
OutputStream output,
FilterMap filterMap,
String sort,
ExportType exportType,
boolean includeConnectionDetails,
boolean includeExamDetails,
final SEBServerUser currentUser);
}

View file

@ -0,0 +1,32 @@
/*
* 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.webservice.servicelayer.exam;
import java.io.OutputStream;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.ExportType;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientConnectionRecord;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientEventRecord;
public interface SEBClientEventExporter {
ExportType exportType();
void streamHeader(
OutputStream output,
boolean includeConnectionDetails,
boolean includeExamDetails);
void streamData(
OutputStream output,
ClientEventRecord eventData,
ClientConnectionRecord connectionData,
Exam examData);
}

View file

@ -44,6 +44,7 @@ import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Cryptor;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.AdditionalAttributeRecord;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.AdditionalAttributesDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO;
@ -249,7 +250,7 @@ public class ExamAdminServiceImpl implements ExamAdminService {
EntityType.EXAM,
examId,
ProctoringServiceSettings.ATTR_SERVER_URL,
proctoringServiceSettings.serverURL);
StringUtils.trim(proctoringServiceSettings.serverURL));
this.additionalAttributesDAO.saveAdditionalAttribute(
EntityType.EXAM,
@ -261,13 +262,13 @@ public class ExamAdminServiceImpl implements ExamAdminService {
EntityType.EXAM,
examId,
ProctoringServiceSettings.ATTR_APP_KEY,
proctoringServiceSettings.appKey);
StringUtils.trim(proctoringServiceSettings.appKey));
this.additionalAttributesDAO.saveAdditionalAttribute(
EntityType.EXAM,
examId,
ProctoringServiceSettings.ATTR_APP_SECRET,
this.cryptor.encrypt(proctoringServiceSettings.appSecret)
this.cryptor.encrypt(Utils.trim(proctoringServiceSettings.appSecret))
.getOrThrow()
.toString());
@ -276,13 +277,13 @@ public class ExamAdminServiceImpl implements ExamAdminService {
EntityType.EXAM,
examId,
ProctoringServiceSettings.ATTR_SDK_KEY,
proctoringServiceSettings.sdkKey);
StringUtils.trim(proctoringServiceSettings.sdkKey));
this.additionalAttributesDAO.saveAdditionalAttribute(
EntityType.EXAM,
examId,
ProctoringServiceSettings.ATTR_SDK_SECRET,
this.cryptor.encrypt(proctoringServiceSettings.sdkSecret)
this.cryptor.encrypt(Utils.trim(proctoringServiceSettings.sdkSecret))
.getOrThrow()
.toString());
}
@ -309,7 +310,8 @@ public class ExamAdminServiceImpl implements ExamAdminService {
EntityType.EXAM,
examId,
ProctoringServiceSettings.ATTR_ENABLE_PROCTORING)
.map(rec -> BooleanUtils.toBoolean(rec.getValue()));
.map(rec -> BooleanUtils.toBoolean(rec.getValue()))
.onError(error -> log.error("Failed to verify proctoring enabled for exam: {}", examId, error));
if (result.hasError()) {
return Result.of(false);
}

View file

@ -0,0 +1,281 @@
/*
* 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.webservice.servicelayer.exam.impl;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.EntityProcessingReport;
import ch.ethz.seb.sebserver.gbl.model.EntityProcessingReport.ErrorEntry;
import ch.ethz.seb.sebserver.gbl.model.Page;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.ExportType;
import ch.ethz.seb.sebserver.gbl.model.user.UserRole;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientEventRecordDynamicSqlSupport;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientConnectionRecord;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientEventRecord;
import ch.ethz.seb.sebserver.webservice.servicelayer.PaginationService;
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.impl.SEBServerUser;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientEventDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.SEBClientEventAdminService;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.SEBClientEventExporter;
@Lazy
@Service
@WebServiceProfile
public class SEBClientEventAdminServiceImpl implements SEBClientEventAdminService {
private static final Logger log = LoggerFactory.getLogger(SEBClientEventAdminServiceImpl.class);
private final PaginationService paginationService;
private final ClientEventDAO clientEventDAO;
private final SEBClientEventExportTransactionHandler sebClientEventExportTransactionHandler;
private final EnumMap<ExportType, SEBClientEventExporter> exporter;
public SEBClientEventAdminServiceImpl(
final PaginationService paginationService,
final ClientEventDAO clientEventDAO,
final SEBClientEventExportTransactionHandler sebClientEventExportTransactionHandler,
final Collection<SEBClientEventExporter> exporter) {
this.paginationService = paginationService;
this.clientEventDAO = clientEventDAO;
this.sebClientEventExportTransactionHandler = sebClientEventExportTransactionHandler;
this.exporter = new EnumMap<>(ExportType.class);
exporter.forEach(exp -> this.exporter.putIfAbsent(exp.exportType(), exp));
}
@Override
public Result<EntityProcessingReport> deleteAllClientEvents(final Collection<String> ids) {
return Result.tryCatch(() -> {
if (ids == null || ids.isEmpty()) {
return EntityProcessingReport.ofEmptyError();
}
final Set<EntityKey> sources = ids.stream()
.map(id -> new EntityKey(id, EntityType.CLIENT_EVENT))
.collect(Collectors.toSet());
final Result<Collection<EntityKey>> delete = this.clientEventDAO.delete(sources);
if (delete.hasError()) {
return new EntityProcessingReport(
Collections.emptyList(),
Collections.emptyList(),
Arrays.asList(new ErrorEntry(null, APIMessage.ErrorMessage.UNEXPECTED.of(delete.getError()))));
} else {
return new EntityProcessingReport(
sources,
delete.get(),
Collections.emptyList());
}
});
}
@Override
public void exportSEBClientLogs(
final OutputStream output,
final FilterMap filterMap,
final String sort,
final ExportType exportType,
final boolean includeConnectionDetails,
final boolean includeExamDetails,
final SEBServerUser currentUser) {
try {
new exportRunner(
this.exporter.get(exportType),
includeConnectionDetails,
includeExamDetails,
new Pager(filterMap, sort),
output)
.run(currentUser);
} catch (final Exception e) {
log.error("Unexpected error during export SEB logs: ", e);
} finally {
try {
output.flush();
} catch (final IOException e) {
e.printStackTrace();
}
try {
output.close();
} catch (final IOException e) {
e.printStackTrace();
}
}
}
private class exportRunner {
private final SEBClientEventExporter exporter;
private final boolean includeConnectionDetails;
private final boolean includeExamDetails;
private final Iterator<Collection<ClientEventRecord>> pager;
private final OutputStream output;
private final Map<Long, Exam> examCache;
private final Map<Long, ClientConnectionRecord> connectionCache;
public exportRunner(
final SEBClientEventExporter exporter,
final boolean includeConnectionDetails,
final boolean includeExamDetails,
final Iterator<Collection<ClientEventRecord>> pager,
final OutputStream output) {
this.exporter = exporter;
this.includeConnectionDetails = includeConnectionDetails;
this.includeExamDetails = includeExamDetails;
this.pager = pager;
this.output = output;
this.connectionCache = new HashMap<>();
this.examCache = new HashMap<>();
}
public void run(final SEBServerUser currentUser) {
final EnumSet<UserRole> userRoles = currentUser.getUserRoles();
final boolean isSupporterOnly = userRoles.size() == 1 && userRoles.contains(UserRole.EXAM_SUPPORTER);
// first stream header line
this.exporter.streamHeader(this.output, this.includeConnectionDetails, this.includeExamDetails);
// then batch with the pager and stream line per line
while (this.pager.hasNext()) {
this.pager.next().forEach(rec -> {
final Exam exam = getExam(rec.getClientConnectionId());
if (!isSupporterOnly || exam.isOwner(currentUser.uuid())) {
this.exporter.streamData(
this.output,
rec,
this.includeConnectionDetails ? getConnection(rec.getClientConnectionId()) : null,
this.includeExamDetails ? getExam(rec.getClientConnectionId()) : null);
}
});
}
}
private ClientConnectionRecord getConnection(final Long connectionId) {
if (!this.connectionCache.containsKey(connectionId)) {
SEBClientEventAdminServiceImpl.this.sebClientEventExportTransactionHandler
.clientConnectionById(connectionId)
.onSuccess(rec -> this.connectionCache.put(rec.getId(), rec))
.onError(error -> log.error("Failed to get ClientConnectionRecord for id: {}",
connectionId,
error));
}
return this.connectionCache.get(connectionId);
}
private Exam getExam(final Long connectionId) {
final ClientConnectionRecord connection = getConnection(connectionId);
final Long examId = connection.getExamId();
if (!this.examCache.containsKey(examId)) {
SEBClientEventAdminServiceImpl.this.sebClientEventExportTransactionHandler
.examById(examId)
.onSuccess(e -> this.examCache.put(examId, e))
.onError(error -> log.error("Failed to get Exam for id: {}",
examId,
error));
}
return this.examCache.get(examId);
}
}
private class Pager implements Iterator<Collection<ClientEventRecord>> {
private final FilterMap filterMap;
private final String sort;
private int pageNumber = 1;
private final int pageSize = 100;
private Collection<ClientEventRecord> nextRecords;
public Pager(
final FilterMap filterMap,
final String sort) {
this.filterMap = filterMap;
this.sort = sort;
fetchNext();
}
@Override
public boolean hasNext() {
return this.nextRecords != null && !this.nextRecords.isEmpty();
}
@Override
public Collection<ClientEventRecord> next() {
final Collection<ClientEventRecord> result = this.nextRecords;
fetchNext();
return result;
}
private void fetchNext() {
try {
final Page<ClientEventRecord> nextPage = SEBClientEventAdminServiceImpl.this.paginationService
.getPageOf(
this.pageNumber,
this.pageSize,
this.sort,
ClientEventRecordDynamicSqlSupport.clientEventRecord.name(),
() -> SEBClientEventAdminServiceImpl.this.sebClientEventExportTransactionHandler
.allMatching(this.filterMap, Utils.truePredicate()))
.getOrThrow();
if (nextPage.getPageNumber() == this.pageNumber) {
this.nextRecords = nextPage.content;
this.pageNumber++;
} else {
this.nextRecords = null;
}
} catch (final Exception e) {
log.error("Failed to fetch next batch: ", e);
this.nextRecords = null;
}
}
}
}

View file

@ -0,0 +1,132 @@
/*
* 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.webservice.servicelayer.exam.impl;
import java.io.IOException;
import java.io.OutputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.EventType;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.ExportType;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientConnectionRecord;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientEventRecord;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.SEBClientEventExporter;
@Lazy
@Component
@WebServiceProfile
public class SEBClientEventCSVExporter implements SEBClientEventExporter {
private static final Logger log = LoggerFactory.getLogger(SEBClientEventCSVExporter.class);
@Override
public ExportType exportType() {
return ExportType.CSV;
}
@Override
public void streamHeader(
final OutputStream output,
final boolean includeConnectionDetails,
final boolean includeExamDetails) {
final StringBuilder builder = new StringBuilder();
builder.append("Event Type,Message,Value,Client Time (UTC),Server Time (UTC)");
if (includeConnectionDetails) {
builder.append(",User Session-ID,Client Machine,Connection Status,Connection Token");
}
if (includeExamDetails) {
builder.append("Exam Name,Exam Description,Exam Type,Start Time (LMS),End Time (LMS)");
}
builder.append(Constants.CARRIAGE_RETURN);
try {
output.write(Utils.toByteArray(builder));
} catch (final IOException e) {
log.error("Failed to stream header: ", e);
} finally {
try {
output.flush();
} catch (final IOException e) {
e.printStackTrace();
}
}
}
@Override
public void streamData(
final OutputStream output,
final ClientEventRecord eventData,
final ClientConnectionRecord connectionData,
final Exam examData) {
final StringBuilder builder = new StringBuilder();
final EventType type = EventType.byId(eventData.getType());
builder.append(type.name());
builder.append(Constants.COMMA);
builder.append(Utils.toCSVString(eventData.getText()));
builder.append(Constants.COMMA);
builder.append(eventData.getNumericValue() != null ? eventData.getNumericValue() : "");
builder.append(Constants.COMMA);
builder.append(Utils.toDateTimeUTC(eventData.getClientTime()));
builder.append(Constants.COMMA);
builder.append(Utils.toDateTimeUTC(eventData.getServerTime()));
if (connectionData != null) {
builder.append(Constants.COMMA);
builder.append(Utils.toCSVString(connectionData.getExamUserSessionId()));
builder.append(Constants.COMMA);
builder.append(Utils.toCSVString(connectionData.getClientAddress()));
builder.append(Constants.COMMA);
builder.append(connectionData.getStatus());
builder.append(Constants.COMMA);
builder.append(connectionData.getConnectionToken());
}
if (examData != null) {
builder.append(Constants.COMMA);
builder.append(Utils.toCSVString(examData.getName()));
builder.append(Constants.COMMA);
builder.append(Utils.toCSVString(examData.getDescription()));
builder.append(Constants.COMMA);
builder.append(examData.getType().name());
builder.append(Constants.COMMA);
builder.append(examData.getStartTime());
builder.append(Constants.COMMA);
builder.append(examData.getEndTime());
}
builder.append(Constants.CARRIAGE_RETURN);
try {
output.write(Utils.toByteArray(builder));
} catch (final IOException e) {
log.error("Failed to stream header: ", e);
} finally {
try {
output.flush();
} catch (final IOException e) {
e.printStackTrace();
}
}
}
}

View file

@ -0,0 +1,113 @@
/*
* 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.webservice.servicelayer.exam.impl;
import static org.mybatis.dynamic.sql.SqlBuilder.equalTo;
import static org.mybatis.dynamic.sql.SqlBuilder.isEqualToWhenPresent;
import java.util.Collection;
import java.util.function.Predicate;
import org.mybatis.dynamic.sql.SqlBuilder;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.EventType;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientConnectionRecordDynamicSqlSupport;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientConnectionRecordMapper;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientEventRecordDynamicSqlSupport;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientEventRecordMapper;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientConnectionRecord;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientEventRecord;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
@Lazy
@Component
@WebServiceProfile
public class SEBClientEventExportTransactionHandler {
private final ClientEventRecordMapper clientEventRecordMapper;
private final ClientConnectionRecordMapper clientConnectionRecordMapper;
private final ExamDAO examDAO;
public SEBClientEventExportTransactionHandler(
final ClientEventRecordMapper clientEventRecordMapper,
final ClientConnectionRecordMapper clientConnectionRecordMapper,
final ExamDAO examDAO) {
this.clientEventRecordMapper = clientEventRecordMapper;
this.clientConnectionRecordMapper = clientConnectionRecordMapper;
this.examDAO = examDAO;
}
@Transactional(readOnly = true)
public Result<Collection<ClientEventRecord>> allMatching(
final FilterMap filterMap,
final Predicate<ClientEvent> predicate) {
return Result.tryCatch(() -> this.clientEventRecordMapper
.selectByExample()
.leftJoin(ClientConnectionRecordDynamicSqlSupport.clientConnectionRecord)
.on(
ClientConnectionRecordDynamicSqlSupport.id,
equalTo(ClientEventRecordDynamicSqlSupport.clientConnectionId))
.where(
ClientConnectionRecordDynamicSqlSupport.institutionId,
isEqualToWhenPresent(filterMap.getInstitutionId()))
.and(
ClientConnectionRecordDynamicSqlSupport.examId,
isEqualToWhenPresent(filterMap.getClientEventExamId()))
.and(
ClientConnectionRecordDynamicSqlSupport.examUserSessionId,
SqlBuilder.isLikeWhenPresent(filterMap.getSQLWildcard(ClientConnection.FILTER_ATTR_SESSION_ID)))
.and(
ClientEventRecordDynamicSqlSupport.clientConnectionId,
isEqualToWhenPresent(filterMap.getClientEventConnectionId()))
.and(
ClientEventRecordDynamicSqlSupport.type,
isEqualToWhenPresent(filterMap.getClientEventTypeId()))
.and(
ClientEventRecordDynamicSqlSupport.type,
SqlBuilder.isNotEqualTo(EventType.LAST_PING.id))
.and(
ClientEventRecordDynamicSqlSupport.clientTime,
SqlBuilder.isGreaterThanOrEqualToWhenPresent(filterMap.getClientEventClientTimeFrom()))
.and(
ClientEventRecordDynamicSqlSupport.clientTime,
SqlBuilder.isLessThanOrEqualToWhenPresent(filterMap.getClientEventClientTimeTo()))
.and(
ClientEventRecordDynamicSqlSupport.serverTime,
SqlBuilder.isGreaterThanOrEqualToWhenPresent(filterMap.getClientEventServerTimeFrom()))
.and(
ClientEventRecordDynamicSqlSupport.serverTime,
SqlBuilder.isLessThanOrEqualToWhenPresent(filterMap.getClientEventServerTimeTo()))
.and(
ClientEventRecordDynamicSqlSupport.text,
SqlBuilder.isLikeWhenPresent(filterMap.getClientEventText()))
.build()
.execute());
}
@Transactional(readOnly = true)
public Result<ClientConnectionRecord> clientConnectionById(final Long id) {
return Result.tryCatch(() -> this.clientConnectionRecordMapper.selectByPrimaryKey(id));
}
public Result<Exam> examById(final Long id) {
return this.examDAO.byPK(id);
}
}

View file

@ -202,7 +202,8 @@ public class SEBRestrictionServiceImpl implements SEBRestrictionService {
.map(sebRestrictionData -> {
if (log.isDebugEnabled()) {
log.debug("Applying SEB Client restriction on LMS with: {}", sebRestrictionData);
log.debug(" *** SEB Restriction *** Applying SEB Client restriction on LMS with: {}",
sebRestrictionData);
}
return this.lmsAPIService
@ -221,7 +222,7 @@ public class SEBRestrictionServiceImpl implements SEBRestrictionService {
public Result<Exam> releaseSEBClientRestriction(final Exam exam) {
if (log.isDebugEnabled()) {
log.debug("Release SEB Client restrictions for exam: {}", exam);
log.debug(" *** SEB Restriction *** Release SEB Client restrictions from LMS for exam: {}", exam);
}
return this.lmsAPIService

View file

@ -236,10 +236,6 @@ public class MockupLmsAPITemplate implements LmsAPITemplate {
this.webserviceInfo.getHttpScheme() +
"://" + externalAddressAlias + "/api/";
if (log.isTraceEnabled()) {
log.trace("Use external address for course access: {}", _externalStartURI);
}
return new QuizData(
quizData.id, quizData.institutionId, quizData.lmsSetupId, quizData.lmsType,
quizData.name, quizData.description, quizData.startTime,

View file

@ -267,7 +267,8 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
// connection integrity check
if (clientConnection.status == ConnectionStatus.ACTIVE) {
if (clientConnection.clientAddress != null && clientConnection.clientAddress.equals(clientAddress)) {
if (clientConnection.clientAddress != null &&
(StringUtils.isBlank(clientAddress) || clientConnection.clientAddress.equals(clientAddress))) {
// It seems that this is the same SEB that tries to establish the connection once again.
// Just log this and return already established connection
if (log.isDebugEnabled()) {

View file

@ -216,7 +216,7 @@ public class SEBClientInstructionServiceImpl implements SEBClientInstructionServ
.toString();
if (log.isDebugEnabled()) {
log.debug("Send SEB client instruction: {} to: {} ", connectionToken, instructionJSON);
log.debug("Send SEB client instruction: {} to: {} ", instructionJSON, connectionToken);
}
return instructionJSON;

View file

@ -77,7 +77,9 @@ public abstract class AbstractPingIndicator extends AbstractClientIndicator {
// Update last ping time on persistent storage
final long millisecondsNow = DateTimeUtils.currentTimeMillis();
if (millisecondsNow - this.lastUpdate > INTERVAL_FOR_PERSISTENT_UPDATE) {
this.distributedPingCache.updatePing(this.pingRecord, millisecondsNow);
synchronized (this) {
this.distributedPingCache.updatePing(this.pingRecord, millisecondsNow);
}
}
}
}

View file

@ -21,9 +21,11 @@ import org.joda.time.DateTimeUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.EventType;
@ -51,13 +53,14 @@ public class DistributedPingCache implements DisposableBean {
final ClientEventLastPingMapper clientEventLastPingMapper,
final ClientEventRecordMapper clientEventRecordMapper,
final WebserviceInfo webserviceInfo,
final TaskScheduler taskScheduler) {
final TaskScheduler taskScheduler,
@Value("${sebserver.webservice.distributed.pingUpdate:3000}") final long pingUpdate) {
this.clientEventLastPingMapper = clientEventLastPingMapper;
this.clientEventRecordMapper = clientEventRecordMapper;
if (webserviceInfo.isDistributed()) {
try {
this.taskRef = taskScheduler.scheduleAtFixedRate(this::updateCache, 1000);
this.taskRef = taskScheduler.scheduleAtFixedRate(this::updateCache, pingUpdate);
} catch (final Exception e) {
log.error("Failed to initialize distributed ping cache update task");
this.taskRef = null;
@ -126,7 +129,6 @@ public class DistributedPingCache implements DisposableBean {
}
}
@Transactional
public void updatePing(final Long pingRecordId, final Long pingTime) {
try {
@ -155,6 +157,8 @@ public class DistributedPingCache implements DisposableBean {
} catch (final Exception e) {
log.error("Failed to delete ping for connection -> {}", connectionId, e);
} finally {
this.pingCache.remove(connectionId);
}
}
@ -180,7 +184,7 @@ public class DistributedPingCache implements DisposableBean {
}
}
@Transactional
@Transactional(readOnly = true, isolation = Isolation.READ_UNCOMMITTED)
public void updateCache() {
if (this.pingCache.isEmpty()) {

View file

@ -282,22 +282,29 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
private void assignToCollectingRoom(final ClientConnectionRecord cc) {
try {
final RemoteProctoringRoom proctoringRoom = getProctoringRoom(
cc.getExamId(),
cc.getConnectionToken());
if (cc.getRemoteProctoringRoomId() == null) {
this.clientConnectionDAO
.assignToProctoringRoom(
cc.getId(),
cc.getConnectionToken(),
proctoringRoom.id)
final RemoteProctoringRoom proctoringRoom = getProctoringRoom(
cc.getExamId(),
cc.getConnectionToken());
if (log.isDebugEnabled()) {
log.debug("Assigning new SEB client to proctoring room: {}, connection: {}",
proctoringRoom.id,
cc);
}
this.clientConnectionDAO
.assignToProctoringRoom(
cc.getId(),
cc.getConnectionToken(),
proctoringRoom.id)
.getOrThrow();
}
applyProcotringInstruction(cc)
.getOrThrow();
applyProcotringInstruction(
cc.getExamId(),
cc.getConnectionToken())
.getOrThrow();
} catch (final Exception e) {
log.error("Failed to assign connection to collecting room: {}", cc, e);
}
@ -598,11 +605,11 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
});
}
private Result<Void> applyProcotringInstruction(
final Long examId,
final String connectionToken) {
private Result<Void> applyProcotringInstruction(final ClientConnectionRecord cc) {
return Result.tryCatch(() -> {
final Long examId = cc.getExamId();
final String connectionToken = cc.getConnectionToken();
final ProctoringServiceSettings settings = this.examAdminService
.getProctoringServiceSettings(examId)
.getOrThrow();
@ -624,17 +631,32 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
townhallRoom.subject)
.getOrThrow();
sendJoinInstruction(
examId,
connectionToken,
roomConnection,
examProctoringService);
try {
sendJoinInstruction(
examId,
connectionToken,
roomConnection,
examProctoringService);
} catch (final Exception e) {
log.error("Failed to send join for town-hall room assignment to connection: {}", cc);
this.clientConnectionDAO
.markForProctoringUpdate(cc.getId())
.onError(error -> log.error("Failed to mark connection for proctoring update: ", error));
}
} else {
try {
sendJoinCollectingRoomInstructions(
settings,
Arrays.asList(connectionToken),
examProctoringService);
sendJoinCollectingRoomInstruction(
settings,
examProctoringService,
connectionToken);
} catch (final Exception e) {
log.error("Failed to send join for collecting room assignment to connection: {}", cc);
this.clientConnectionDAO
.markForProctoringUpdate(cc.getId())
.onError(error -> log.error("Failed to mark connection for proctoring update: ", error));
}
}
});
@ -653,23 +675,30 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
clientConnectionTokens
.stream()
.forEach(connectionToken -> {
final ProctoringRoomConnection proctoringConnection = examProctoringService
.getClientRoomConnection(
proctoringSettings,
try {
final ProctoringRoomConnection proctoringConnection = examProctoringService
.getClientRoomConnection(
proctoringSettings,
connectionToken,
roomName,
(StringUtils.isNotBlank(subject)) ? subject : roomName)
.onError(error -> log.error(
"Failed to get client room connection data for {} cause: {}",
connectionToken,
error.getMessage()))
.get();
if (proctoringConnection != null) {
sendJoinInstruction(
proctoringSettings.examId,
connectionToken,
roomName,
(StringUtils.isNotBlank(subject)) ? subject : roomName)
.onError(error -> log.error(
"Failed to get client room connection data for {} cause: {}",
connectionToken,
error.getMessage()))
.get();
if (proctoringConnection != null) {
sendJoinInstruction(
proctoringSettings.examId,
connectionToken,
proctoringConnection,
examProctoringService);
proctoringConnection,
examProctoringService);
}
} catch (final Exception e) {
log.error("Failed to send join to break-out room: {} connection: {}",
roomName,
roomName,
e);
}
});
@ -688,10 +717,18 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
clientConnectionTokens
.stream()
.forEach(connectionToken -> sendJoinCollectingRoomInstruction(
proctoringSettings,
examProctoringService,
connectionToken));
.forEach(connectionToken -> {
try {
sendJoinCollectingRoomInstruction(
proctoringSettings,
examProctoringService,
connectionToken);
} catch (final Exception e) {
log.error(
"Failed to send proctoring room join instruction to single client. Skip and proceed with other clients. {}",
e.getMessage());
}
});
}
private void sendJoinCollectingRoomInstruction(
@ -721,8 +758,10 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
clientConnection.clientConnection.connectionToken,
proctoringConnection,
examProctoringService);
} catch (final Exception e) {
log.error("Failed to send proctoring room join instruction to client: {}", connectionToken, e);
throw e;
}
}
@ -732,6 +771,12 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
final ProctoringRoomConnection proctoringConnection,
final ExamProctoringService examProctoringService) {
if (log.isDebugEnabled()) {
log.debug("Send proctoring join instruction to connection: {}, room: {}",
connectionToken,
proctoringConnection.roomName);
}
final Map<String, String> attributes = examProctoringService
.createJoinInstructionAttributes(proctoringConnection);
@ -742,7 +787,8 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
attributes,
connectionToken,
true)
.onError(error -> log.error("Failed to send join instruction: {}", connectionToken, error));
.onError(error -> log.error("Failed to send join instruction: {}", connectionToken, error))
.getOrThrow();
}
}

View file

@ -623,6 +623,10 @@ public class ZoomProctoringService implements ExamProctoringService {
credentials.clientIdAsString(),
expTime);
if (log.isTraceEnabled()) {
log.trace("Zoom API Token payload: {}", jwtPayload);
}
final String jwtPayloadPart = urlEncoder
.encodeToString(jwtPayload.getBytes(StandardCharsets.UTF_8));
@ -675,6 +679,10 @@ public class ZoomProctoringService implements ExamProctoringService {
expTime,
expTime);
if (log.isTraceEnabled()) {
log.trace("Zoom SDK Token payload: {}", jwtPayload);
}
final String jwtPayloadPart = urlEncoder
.encodeToString(jwtPayload.getBytes(StandardCharsets.UTF_8));
@ -724,6 +732,10 @@ public class ZoomProctoringService implements ExamProctoringService {
final String tmpString = String.format("%s.%s.%s.%d.%s", apiKey, meetingId, ts, status, hashBase64Str);
final String encodedString = Base64.getEncoder().encodeToString(tmpString.getBytes());
if (log.isTraceEnabled()) {
log.trace("Zoom Meeting signature payload: {}", tmpString);
}
return encodedString.replaceAll("\\=+$", "");
} catch (final Exception e) {
@ -732,12 +744,16 @@ public class ZoomProctoringService implements ExamProctoringService {
}
private long forExam(final ProctoringServiceSettings examProctoring) {
if (examProctoring.examId == null) {
throw new IllegalStateException("Missing exam identifier from ExamProctoring data");
}
long expTime = Utils.toSeconds(System.currentTimeMillis() + Constants.DAY_IN_MILLIS);
if (this.examSessionService.isExamRunning(examProctoring.examId)) {
// TODO
// NOTE: following is the original code that includes the exam end time but seems to make trouble for OLAT
final long nowInSeconds = Utils.getSecondsNow();
final long nowPlus30MinInSeconds = nowInSeconds + Utils.toSeconds(30 * Constants.MINUTE_IN_MILLIS);
final long nowPlusOneDayInSeconds = nowInSeconds + Utils.toSeconds(Constants.DAY_IN_MILLIS);
final long nowPlusTwoDayInSeconds = nowInSeconds + Utils.toSeconds(2 * Constants.DAY_IN_MILLIS);
long expTime = nowPlusOneDayInSeconds;
if (examProctoring.examId == null && this.examSessionService.isExamRunning(examProctoring.examId)) {
final Exam exam = this.examSessionService.getRunningExam(examProctoring.examId)
.getOrThrow();
if (exam.endTime != null) {
@ -746,10 +762,16 @@ public class ZoomProctoringService implements ExamProctoringService {
}
// refer to https://marketplace.zoom.us/docs/sdk/native-sdks/auth
// "exp": 0, //JWT expiration date (Min:1800 seconds greater than iat value, Max: 48 hours greater than iat value) in epoch format.
if (expTime - Utils.getSecondsNow() > Utils.toSeconds(2 * Constants.DAY_IN_MILLIS)) {
expTime = Utils.toSeconds(System.currentTimeMillis() + Constants.DAY_IN_MILLIS);
if (expTime > nowPlusTwoDayInSeconds) {
expTime = nowPlusTwoDayInSeconds - 10; // Do not set to max because it is not well defined if max is included or not
} else if (expTime < nowPlus30MinInSeconds) {
expTime = nowPlusOneDayInSeconds;
}
return expTime;
log.debug("**** SDK Token exp time with exam-end-time inclusion would be: {}", expTime);
// NOTE: Set this to the maximum according to https://marketplace.zoom.us/docs/sdk/native-sdks/auth
return nowPlusTwoDayInSeconds - 10; // Do not set to max because it is not well defined if max is included or not;
}
private final static class ZoomRestTemplate {

View file

@ -8,17 +8,20 @@
package ch.ethz.seb.sebserver.webservice.weblayer.api;
import java.util.Arrays;
import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.IOUtils;
import org.mybatis.dynamic.sql.SqlTable;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.RequestMapping;
@ -28,16 +31,14 @@ import org.springframework.web.bind.annotation.RestController;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.API.BulkActionType;
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.api.authorization.PrivilegeType;
import ch.ethz.seb.sebserver.gbl.model.EntityDependency;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.EntityProcessingReport;
import ch.ethz.seb.sebserver.gbl.model.EntityProcessingReport.ErrorEntry;
import ch.ethz.seb.sebserver.gbl.model.GrantEntity;
import ch.ethz.seb.sebserver.gbl.model.Page;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.ExportType;
import ch.ethz.seb.sebserver.gbl.model.session.ExtendedClientEvent;
import ch.ethz.seb.sebserver.gbl.model.user.UserRole;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
@ -53,6 +54,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientEventDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.SEBClientEventAdminService;
import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationService;
@WebServiceProfile
@ -62,6 +64,7 @@ public class ClientEventController extends ReadonlyEntityController<ClientEvent,
private final ExamDAO examDao;
private final ClientEventDAO clientEventDAO;
private final SEBClientEventAdminService sebClientEventAdminService;
protected ClientEventController(
final AuthorizationService authorization,
@ -70,7 +73,8 @@ public class ClientEventController extends ReadonlyEntityController<ClientEvent,
final UserActivityLogDAO userActivityLogDAO,
final PaginationService paginationService,
final BeanValidationService beanValidationService,
final ExamDAO examDao) {
final ExamDAO examDao,
final SEBClientEventAdminService sebClientEventAdminService) {
super(authorization,
bulkActionService,
@ -81,6 +85,7 @@ public class ClientEventController extends ReadonlyEntityController<ClientEvent,
this.examDao = examDao;
this.clientEventDAO = entityDAO;
this.sebClientEventAdminService = sebClientEventAdminService;
}
@RequestMapping(
@ -136,26 +141,69 @@ public class ClientEventController extends ReadonlyEntityController<ClientEvent,
this.checkWritePrivilege(institutionId);
if (ids == null || ids.isEmpty()) {
return EntityProcessingReport.ofEmptyError();
}
return this.sebClientEventAdminService
.deleteAllClientEvents(ids)
.getOrThrow();
}
final Set<EntityKey> sources = ids.stream()
.map(id -> new EntityKey(id, EntityType.CLIENT_EVENT))
.collect(Collectors.toSet());
@RequestMapping(
path = API.SEB_CLIENT_EVENT_EXPORT_PATH_SEGMENT,
method = RequestMethod.GET,
produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public void exportEvents(
@RequestParam(
name = API.PARAM_INSTITUTION_ID,
required = true,
defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId,
@RequestParam(name = API.SEB_CLIENT_EVENT_EXPORT_TYPE, required = true) final ExportType type,
@RequestParam(
name = API.SEB_CLIENT_EVENT_EXPORT_INCLUDE_CONNECTIONS,
required = false,
defaultValue = "true") final boolean includeConnectionDetails,
@RequestParam(
name = API.SEB_CLIENT_EVENT_EXPORT_INCLUDE_EXAMS,
required = false,
defaultValue = "false") final boolean includeExamDetails,
@RequestParam(name = Page.ATTR_SORT, required = false) final String sort,
@RequestParam final MultiValueMap<String, String> allRequestParams,
final HttpServletRequest request,
final HttpServletResponse response) throws IOException {
final Result<Collection<EntityKey>> delete = this.clientEventDAO.delete(sources);
// at least current user must have base read access for specified entity type within its own institution
checkReadPrivilege(institutionId);
if (delete.hasError()) {
return new EntityProcessingReport(
Collections.emptyList(),
Collections.emptyList(),
Arrays.asList(new ErrorEntry(null, APIMessage.ErrorMessage.UNEXPECTED.of(delete.getError()))));
} else {
return new EntityProcessingReport(
sources,
delete.get(),
Collections.emptyList());
final FilterMap filterMap = new FilterMap(allRequestParams, request.getQueryString());
populateFilterMap(filterMap, institutionId, sort);
final ServletOutputStream outputStream = response.getOutputStream();
PipedOutputStream pout;
PipedInputStream pin;
try {
pout = new PipedOutputStream();
pin = new PipedInputStream(pout);
final SEBServerUser currentUser = this.authorization
.getUserService()
.getCurrentUser();
this.sebClientEventAdminService.exportSEBClientLogs(
pout,
filterMap,
sort,
type,
includeConnectionDetails,
includeExamDetails,
currentUser);
IOUtils.copyLarge(pin, outputStream);
response.setStatus(HttpStatus.OK.value());
outputStream.flush();
} finally {
outputStream.flush();
outputStream.close();
}
}
@ -175,12 +223,14 @@ public class ClientEventController extends ReadonlyEntityController<ClientEvent,
@Override
protected Result<ClientEvent> checkReadAccess(final ClientEvent entity) {
final EnumSet<UserRole> userRoles = this.authorization
.getUserService()
.getCurrentUser()
.getUserRoles();
final boolean isSupporterOnly = userRoles.size() == 1 && userRoles.contains(UserRole.EXAM_SUPPORTER);
return Result.tryCatch(() -> {
final EnumSet<UserRole> userRoles = this.authorization
.getUserService()
.getCurrentUser()
.getUserRoles();
final boolean isSupporterOnly = userRoles.size() == 1 && userRoles.contains(UserRole.EXAM_SUPPORTER);
if (isSupporterOnly) {
// check owner grant be getting exam
return super.checkReadAccess(entity)

View file

@ -131,7 +131,8 @@ public class ExamAPI_V1_Controller {
.map(this::createRunningExamInfo)
.collect(Collectors.toList());
} else {
final Exam exam = this.examSessionService.getExamDAO().byPK(examId)
final Exam exam = this.examSessionService.getExamDAO()
.byPK(examId)
.getOrThrow();
result = Arrays.asList(createRunningExamInfo(exam));

View file

@ -327,7 +327,7 @@ public class ExamProctoringController {
this.authorizationService.checkRead(this.examSessionService
.getExamDAO()
.byPK(examId)
.examGrantEntityByPK(examId)
.getOrThrow());
}

View file

@ -72,11 +72,10 @@ public class IndicatorController extends EntityController<Indicator, Indicator>
@Override
protected Indicator createNew(final POSTMapper postParams) {
final Long examId = postParams.getLong(Domain.INDICATOR.ATTR_EXAM_ID);
return this.examDao
.byPK(examId)
.map(exam -> new Indicator(exam, postParams))
.getOrThrow();
if (examId == null) {
throw new RuntimeException("Missing exam model id from request parameter map!");
}
return new Indicator(examId, postParams);
}
@Override
@ -120,7 +119,8 @@ public class IndicatorController extends EntityController<Indicator, Indicator>
return null;
}
return this.examDao.byPK(entity.examId)
return this.examDao
.examGrantEntityByPK(entity.examId)
.getOrThrow();
}

View file

@ -13,6 +13,7 @@ spring.datasource.hikari.initializationFailTimeout=30000
spring.datasource.hikari.connectionTimeout=30000
spring.datasource.hikari.idleTimeout=600000
spring.datasource.hikari.maxLifetime=1800000
spring.datasource.hikari.maximumPoolSize=500
sebserver.http.client.connect-timeout=15000
sebserver.http.client.connection-request-timeout=10000
@ -22,7 +23,7 @@ sebserver.webservice.clean-db-on-startup=false
# webservice configuration
sebserver.init.adminaccount.gen-on-init=false
sebserver.webservice.distributed=true
sebserver.webservice.distributed=false
sebserver.webservice.master.delay.threshold=10000
sebserver.webservice.http.external.scheme=http
sebserver.webservice.http.external.servername=localhost

View file

@ -10,8 +10,9 @@ server.tomcat.uri-encoding=UTF-8
logging.level.ch=INFO
logging.level.ch.ethz.seb.sebserver.webservice.datalayer=INFO
logging.level.org.springframework.cache=INFO
logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl=DEBUG
logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl=TRACE
logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.session=DEBUG
logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.proctoring=TRACE
#logging.level.ch.ethz.seb.sebserver.webservice.weblayer.api.ExamAPI_V1_Controller=TRACE
sebserver.http.client.connect-timeout=150000

View file

@ -25,7 +25,7 @@ sebserver.gui.entrypoint=/gui
sebserver.gui.webservice.apipath=${sebserver.webservice.api.admin.endpoint}
# defines the polling interval that is used to poll the webservice for client connection data on a monitored exam page
sebserver.gui.webservice.poll-interval=1000
sebserver.gui.webservice.poll-interval=3000
sebserver.gui.webservice.mock-lms-enabled=true
sebserver.gui.webservice.edx-lms-enabled=true
sebserver.gui.webservice.moodle-lms-enabled=true

View file

@ -10,6 +10,9 @@ sebserver.init.adminaccount.username=sebserver-admin
sebserver.init.database.integrity.checks=true
sebserver.init.database.integrity.try-fix=true
sebserver.webservice.distributed=false
sebserver.webservice.distributed.pingUpdate=3000
### webservice caching
spring.cache.jcache.provider=org.ehcache.jsr107.EhcacheCachingProvider
spring.cache.jcache.config=classpath:config/ehcache.xml
@ -28,6 +31,7 @@ spring.datasource.hikari.initializationFailTimeout=3000
spring.datasource.hikari.connectionTimeout=30000
spring.datasource.hikari.idleTimeout=600000
spring.datasource.hikari.maxLifetime=1800000
spring.datasource.hikari.maximumPoolSize=500
### webservice security
spring.datasource.password=${sebserver.mariadb.password}

View file

@ -1867,6 +1867,7 @@ sebserver.seblogs.form.column.exam.endTime=End Time
sebserver.seblogs.form.column.exam.endTime.tooltip=The end date and time of the exam
sebserver.seblogs.action.delete=Delete Logs
sebserver.seblogs.action.export.csv=Export CSV
sebserver.seblogs.delete.form.title=Delete SEB Logs
sebserver.seblogs.delete.form.info=This will delete all SEB client logs from the current filtered list.<br/>Please check carefully if all SEB client logs from the list shall be deleted.<br/><br/>There are currently {0} logs within the list.
sebserver.seblogs.delete.action.delete=Delete All Logs

View file

@ -51,7 +51,8 @@ public class ExamProctoringRoomServiceTest extends AdministrationAPIIntegrationT
assertFalse(runningExamsForInstitution.hasError());
final Collection<Exam> collection = runningExamsForInstitution.get();
assertFalse(collection.isEmpty());
final Exam exam = collection.iterator().next();
final Exam exam = collection.stream().filter(e -> e.id == 2L).findAny().orElse(null);
assertNotNull(exam);
assertEquals("Demo Quiz 6 (MOCKUP)", exam.name);
assertEquals("2", String.valueOf(exam.id));
}