SEBSERV-191 finished back-end implementation

This commit is contained in:
anhefti 2021-11-03 13:18:40 +01:00
parent 2f2a3670b7
commit c414586fec
6 changed files with 152 additions and 27 deletions

View file

@ -197,6 +197,8 @@ public final class API {
public static final String SEB_CLIENT_EVENT_ENDPOINT = "/seb-client-event"; 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_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_EXTENDED_PAGE_ENDPOINT = SEB_CLIENT_EVENT_ENDPOINT public static final String SEB_CLIENT_EVENT_EXTENDED_PAGE_ENDPOINT = SEB_CLIENT_EVENT_ENDPOINT
+ SEB_CLIENT_EVENT_SEARCH_PATH_SEGMENT; + SEB_CLIENT_EVENT_SEARCH_PATH_SEGMENT;

View file

@ -666,4 +666,11 @@ public final class Utils {
} }
} }
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

@ -10,13 +10,11 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.exam;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.Collection; import java.util.Collection;
import java.util.function.Predicate;
import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.Async;
import ch.ethz.seb.sebserver.gbl.async.AsyncServiceSpringConfig; import ch.ethz.seb.sebserver.gbl.async.AsyncServiceSpringConfig;
import ch.ethz.seb.sebserver.gbl.model.EntityProcessingReport; import ch.ethz.seb.sebserver.gbl.model.EntityProcessingReport;
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.ClientEvent.ExportType;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
@ -30,7 +28,6 @@ public interface SEBClientEventAdminService {
OutputStream output, OutputStream output,
FilterMap filterMap, FilterMap filterMap,
String sort, String sort,
final Predicate<ClientEvent> predicate,
ExportType exportType, ExportType exportType,
boolean includeConnectionDetails, boolean includeConnectionDetails,
boolean includeExamDetails); boolean includeExamDetails);

View file

@ -13,11 +13,11 @@ import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.EnumMap; import java.util.EnumMap;
import java.util.EnumSet;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -31,14 +31,17 @@ import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.EntityProcessingReport; import ch.ethz.seb.sebserver.gbl.model.EntityProcessingReport;
import ch.ethz.seb.sebserver.gbl.model.EntityProcessingReport.ErrorEntry; import ch.ethz.seb.sebserver.gbl.model.EntityProcessingReport.ErrorEntry;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam; import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
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.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.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result; 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.mapper.ClientEventRecordDynamicSqlSupport;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientConnectionRecord; 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.datalayer.batis.model.ClientEventRecord;
import ch.ethz.seb.sebserver.webservice.servicelayer.PaginationService; import ch.ethz.seb.sebserver.webservice.servicelayer.PaginationService;
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.AuthorizationService;
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.ClientEventDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; 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.SEBClientEventAdminService;
@ -55,16 +58,19 @@ public class SEBClientEventAdminServiceImpl implements SEBClientEventAdminServic
private final ClientEventDAO clientEventDAO; private final ClientEventDAO clientEventDAO;
private final SEBClientEventExportTransactionHandler sebClientEventExportTransactionHandler; private final SEBClientEventExportTransactionHandler sebClientEventExportTransactionHandler;
private final EnumMap<ExportType, SEBClientEventExporter> exporter; private final EnumMap<ExportType, SEBClientEventExporter> exporter;
private final AuthorizationService authorizationService;
public SEBClientEventAdminServiceImpl( public SEBClientEventAdminServiceImpl(
final PaginationService paginationService, final PaginationService paginationService,
final ClientEventDAO clientEventDAO, final ClientEventDAO clientEventDAO,
final SEBClientEventExportTransactionHandler sebClientEventExportTransactionHandler, final SEBClientEventExportTransactionHandler sebClientEventExportTransactionHandler,
final Collection<SEBClientEventExporter> exporter) { final Collection<SEBClientEventExporter> exporter,
final AuthorizationService authorizationService) {
this.paginationService = paginationService; this.paginationService = paginationService;
this.clientEventDAO = clientEventDAO; this.clientEventDAO = clientEventDAO;
this.sebClientEventExportTransactionHandler = sebClientEventExportTransactionHandler; this.sebClientEventExportTransactionHandler = sebClientEventExportTransactionHandler;
this.authorizationService = authorizationService;
this.exporter = new EnumMap<>(ExportType.class); this.exporter = new EnumMap<>(ExportType.class);
exporter.forEach(exp -> this.exporter.putIfAbsent(exp.exportType(), exp)); exporter.forEach(exp -> this.exporter.putIfAbsent(exp.exportType(), exp));
@ -102,7 +108,6 @@ public class SEBClientEventAdminServiceImpl implements SEBClientEventAdminServic
final OutputStream output, final OutputStream output,
final FilterMap filterMap, final FilterMap filterMap,
final String sort, final String sort,
final Predicate<ClientEvent> predicate,
final ExportType exportType, final ExportType exportType,
final boolean includeConnectionDetails, final boolean includeConnectionDetails,
final boolean includeExamDetails) { final boolean includeExamDetails) {
@ -111,7 +116,7 @@ public class SEBClientEventAdminServiceImpl implements SEBClientEventAdminServic
this.exporter.get(exportType), this.exporter.get(exportType),
includeConnectionDetails, includeConnectionDetails,
includeExamDetails, includeExamDetails,
new Pager(filterMap, sort, predicate), new Pager(filterMap, sort),
output) output)
.run(); .run();
@ -147,17 +152,28 @@ public class SEBClientEventAdminServiceImpl implements SEBClientEventAdminServic
public void run() { public void run() {
final SEBServerUser currentUser = SEBClientEventAdminServiceImpl.this.authorizationService
.getUserService()
.getCurrentUser();
final EnumSet<UserRole> userRoles = currentUser.getUserRoles();
final boolean isSupporterOnly = userRoles.size() == 1 && userRoles.contains(UserRole.EXAM_SUPPORTER);
// first stream header line // first stream header line
this.exporter.streamHeader(this.output, this.includeConnectionDetails, this.includeExamDetails); this.exporter.streamHeader(this.output, this.includeConnectionDetails, this.includeExamDetails);
// then batch with the pager and stream line per line // then batch with the pager and stream line per line
while (this.pager.hasNext()) { while (this.pager.hasNext()) {
this.pager.next().forEach(rec -> { this.pager.next().forEach(rec -> {
this.exporter.streamData(
this.output, final Exam exam = getExam(rec.getClientConnectionId());
rec,
this.includeConnectionDetails ? getConnection(rec.getClientConnectionId()) : null, if (!isSupporterOnly || exam.isOwner(currentUser.uuid())) {
this.includeExamDetails ? getExam(rec.getClientConnectionId()) : null); this.exporter.streamData(
this.output,
rec,
this.includeConnectionDetails ? getConnection(rec.getClientConnectionId()) : null,
this.includeExamDetails ? getExam(rec.getClientConnectionId()) : null);
}
}); });
} }
} }
@ -195,7 +211,6 @@ public class SEBClientEventAdminServiceImpl implements SEBClientEventAdminServic
private final FilterMap filterMap; private final FilterMap filterMap;
private final String sort; private final String sort;
private final Predicate<ClientEvent> predicate;
private int pageNumber = 0; private int pageNumber = 0;
private final int pageSize = 100; private final int pageSize = 100;
@ -204,12 +219,10 @@ public class SEBClientEventAdminServiceImpl implements SEBClientEventAdminServic
public Pager( public Pager(
final FilterMap filterMap, final FilterMap filterMap,
final String sort, final String sort) {
final Predicate<ClientEvent> predicate) {
this.filterMap = filterMap; this.filterMap = filterMap;
this.sort = sort; this.sort = sort;
this.predicate = predicate;
fetchNext(); fetchNext();
} }
@ -235,7 +248,7 @@ public class SEBClientEventAdminServiceImpl implements SEBClientEventAdminServic
this.sort, this.sort,
ClientEventRecordDynamicSqlSupport.clientEventRecord.name(), ClientEventRecordDynamicSqlSupport.clientEventRecord.name(),
() -> SEBClientEventAdminServiceImpl.this.sebClientEventExportTransactionHandler () -> SEBClientEventAdminServiceImpl.this.sebClientEventExportTransactionHandler
.allMatching(this.filterMap, this.predicate)) .allMatching(this.filterMap, Utils.truePredicate()))
.getOrThrow(); .getOrThrow();
this.pageNumber++; this.pageNumber++;

View file

@ -8,16 +8,25 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.exam.impl; package ch.ethz.seb.sebserver.webservice.servicelayer.exam.impl;
import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam; 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.model.session.ClientEvent.ExportType;
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.ClientConnectionRecord;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientEventRecord; import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientEventRecord;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.SEBClientEventExporter; import ch.ethz.seb.sebserver.webservice.servicelayer.exam.SEBClientEventExporter;
public class SEBClientEventCSVExporter implements SEBClientEventExporter { public class SEBClientEventCSVExporter implements SEBClientEventExporter {
private static final Logger log = LoggerFactory.getLogger(SEBClientEventCSVExporter.class);
@Override @Override
public ExportType exportType() { public ExportType exportType() {
return ExportType.CSV; return ExportType.CSV;
@ -29,7 +38,24 @@ public class SEBClientEventCSVExporter implements SEBClientEventExporter {
final boolean includeConnectionDetails, final boolean includeConnectionDetails,
final boolean includeExamDetails) { final boolean includeExamDetails) {
// TODO 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);
}
} }
@Override @Override
@ -39,11 +65,37 @@ public class SEBClientEventCSVExporter implements SEBClientEventExporter {
final ClientConnectionRecord connectionData, final ClientConnectionRecord connectionData,
final Exam examData) { final Exam examData) {
// TODO final StringBuilder builder = new StringBuilder();
} final EventType type = EventType.byId(eventData.getType());
private String toCSVString(final String text) { builder.append(type.name());
builder.append(Utils.toCSVString(eventData.getText()));
builder.append(eventData.getNumericValue());
builder.append(Utils.toDateTimeUTC(eventData.getClientTime()));
builder.append(Utils.toDateTimeUTC(eventData.getServerTime()));
if (connectionData != null) {
builder.append(Utils.toCSVString(connectionData.getExamUserSessionId()));
builder.append(Utils.toCSVString(connectionData.getClientAddress()));
builder.append(connectionData.getStatus());
builder.append(connectionData.getConnectionToken());
}
if (examData != null) {
builder.append(Utils.toCSVString(examData.getName()));
builder.append(Utils.toCSVString(examData.getDescription()));
builder.append(examData.getType().name());
builder.append(examData.getStartTime());
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);
}
} }
} }

View file

@ -8,13 +8,19 @@
package ch.ethz.seb.sebserver.webservice.weblayer.api; package ch.ethz.seb.sebserver.webservice.weblayer.api;
import java.io.IOException;
import java.util.Collection; import java.util.Collection;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.List; import java.util.List;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.mybatis.dynamic.sql.SqlTable; import org.mybatis.dynamic.sql.SqlTable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.util.MultiValueMap; import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
@ -31,6 +37,7 @@ import ch.ethz.seb.sebserver.gbl.model.EntityProcessingReport;
import ch.ethz.seb.sebserver.gbl.model.GrantEntity; import ch.ethz.seb.sebserver.gbl.model.GrantEntity;
import ch.ethz.seb.sebserver.gbl.model.Page; 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;
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.session.ExtendedClientEvent;
import ch.ethz.seb.sebserver.gbl.model.user.UserRole; import ch.ethz.seb.sebserver.gbl.model.user.UserRole;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
@ -54,6 +61,8 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationSe
@RequestMapping("${sebserver.webservice.api.admin.endpoint}" + API.SEB_CLIENT_EVENT_ENDPOINT) @RequestMapping("${sebserver.webservice.api.admin.endpoint}" + API.SEB_CLIENT_EVENT_ENDPOINT)
public class ClientEventController extends ReadonlyEntityController<ClientEvent, ClientEvent> { public class ClientEventController extends ReadonlyEntityController<ClientEvent, ClientEvent> {
private static final Logger log = LoggerFactory.getLogger(ClientEventController.class);
private final ExamDAO examDao; private final ExamDAO examDao;
private final ClientEventDAO clientEventDAO; private final ClientEventDAO clientEventDAO;
private final SEBClientEventAdminService sebClientEventAdminService; private final SEBClientEventAdminService sebClientEventAdminService;
@ -138,6 +147,49 @@ public class ClientEventController extends ReadonlyEntityController<ClientEvent,
.getOrThrow(); .getOrThrow();
} }
@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 = Page.ATTR_SORT, required = false) final String sort,
@RequestParam final MultiValueMap<String, String> allRequestParams,
final HttpServletRequest request,
final HttpServletResponse response) throws IOException {
// at least current user must have base read access for specified entity type within its own institution
checkReadPrivilege(institutionId);
final FilterMap filterMap = new FilterMap(allRequestParams, request.getQueryString());
populateFilterMap(filterMap, institutionId, sort);
final ServletOutputStream outputStream = response.getOutputStream();
try {
this.sebClientEventAdminService.exportSEBClientLogs(
outputStream,
filterMap,
sort,
type,
false,
false);
response.setStatus(HttpStatus.OK.value());
} catch (final Exception e) {
log.error("Unexpected error while trying to export SEB client logs: ", e);
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
} finally {
outputStream.flush();
outputStream.close();
}
}
@Override @Override
public Collection<EntityDependency> getDependencies( public Collection<EntityDependency> getDependencies(
final String modelId, final String modelId,
@ -154,12 +206,14 @@ public class ClientEventController extends ReadonlyEntityController<ClientEvent,
@Override @Override
protected Result<ClientEvent> checkReadAccess(final ClientEvent entity) { 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(() -> { 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) { if (isSupporterOnly) {
// check owner grant be getting exam // check owner grant be getting exam
return super.checkReadAccess(entity) return super.checkReadAccess(entity)