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_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
+ 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.util.Collection;
import java.util.function.Predicate;
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;
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.dao.FilterMap;
@ -30,7 +28,6 @@ public interface SEBClientEventAdminService {
OutputStream output,
FilterMap filterMap,
String sort,
final Predicate<ClientEvent> predicate,
ExportType exportType,
boolean includeConnectionDetails,
boolean includeExamDetails);

View file

@ -13,11 +13,11 @@ 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.function.Predicate;
import java.util.stream.Collectors;
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.ErrorEntry;
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.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.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.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.SEBClientEventAdminService;
@ -55,16 +58,19 @@ public class SEBClientEventAdminServiceImpl implements SEBClientEventAdminServic
private final ClientEventDAO clientEventDAO;
private final SEBClientEventExportTransactionHandler sebClientEventExportTransactionHandler;
private final EnumMap<ExportType, SEBClientEventExporter> exporter;
private final AuthorizationService authorizationService;
public SEBClientEventAdminServiceImpl(
final PaginationService paginationService,
final ClientEventDAO clientEventDAO,
final SEBClientEventExportTransactionHandler sebClientEventExportTransactionHandler,
final Collection<SEBClientEventExporter> exporter) {
final Collection<SEBClientEventExporter> exporter,
final AuthorizationService authorizationService) {
this.paginationService = paginationService;
this.clientEventDAO = clientEventDAO;
this.sebClientEventExportTransactionHandler = sebClientEventExportTransactionHandler;
this.authorizationService = authorizationService;
this.exporter = new EnumMap<>(ExportType.class);
exporter.forEach(exp -> this.exporter.putIfAbsent(exp.exportType(), exp));
@ -102,7 +108,6 @@ public class SEBClientEventAdminServiceImpl implements SEBClientEventAdminServic
final OutputStream output,
final FilterMap filterMap,
final String sort,
final Predicate<ClientEvent> predicate,
final ExportType exportType,
final boolean includeConnectionDetails,
final boolean includeExamDetails) {
@ -111,7 +116,7 @@ public class SEBClientEventAdminServiceImpl implements SEBClientEventAdminServic
this.exporter.get(exportType),
includeConnectionDetails,
includeExamDetails,
new Pager(filterMap, sort, predicate),
new Pager(filterMap, sort),
output)
.run();
@ -147,17 +152,28 @@ public class SEBClientEventAdminServiceImpl implements SEBClientEventAdminServic
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
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 -> {
this.exporter.streamData(
this.output,
rec,
this.includeConnectionDetails ? getConnection(rec.getClientConnectionId()) : null,
this.includeExamDetails ? getExam(rec.getClientConnectionId()) : null);
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);
}
});
}
}
@ -195,7 +211,6 @@ public class SEBClientEventAdminServiceImpl implements SEBClientEventAdminServic
private final FilterMap filterMap;
private final String sort;
private final Predicate<ClientEvent> predicate;
private int pageNumber = 0;
private final int pageSize = 100;
@ -204,12 +219,10 @@ public class SEBClientEventAdminServiceImpl implements SEBClientEventAdminServic
public Pager(
final FilterMap filterMap,
final String sort,
final Predicate<ClientEvent> predicate) {
final String sort) {
this.filterMap = filterMap;
this.sort = sort;
this.predicate = predicate;
fetchNext();
}
@ -235,7 +248,7 @@ public class SEBClientEventAdminServiceImpl implements SEBClientEventAdminServic
this.sort,
ClientEventRecordDynamicSqlSupport.clientEventRecord.name(),
() -> SEBClientEventAdminServiceImpl.this.sebClientEventExportTransactionHandler
.allMatching(this.filterMap, this.predicate))
.allMatching(this.filterMap, Utils.truePredicate()))
.getOrThrow();
this.pageNumber++;

View file

@ -8,16 +8,25 @@
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 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.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;
public class SEBClientEventCSVExporter implements SEBClientEventExporter {
private static final Logger log = LoggerFactory.getLogger(SEBClientEventCSVExporter.class);
@Override
public ExportType exportType() {
return ExportType.CSV;
@ -29,7 +38,24 @@ public class SEBClientEventCSVExporter implements SEBClientEventExporter {
final boolean includeConnectionDetails,
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
@ -39,11 +65,37 @@ public class SEBClientEventCSVExporter implements SEBClientEventExporter {
final ClientConnectionRecord connectionData,
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;
import java.io.IOException;
import java.util.Collection;
import java.util.EnumSet;
import java.util.List;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
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.util.MultiValueMap;
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.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;
@ -54,6 +61,8 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationSe
@RequestMapping("${sebserver.webservice.api.admin.endpoint}" + API.SEB_CLIENT_EVENT_ENDPOINT)
public class ClientEventController extends ReadonlyEntityController<ClientEvent, ClientEvent> {
private static final Logger log = LoggerFactory.getLogger(ClientEventController.class);
private final ExamDAO examDao;
private final ClientEventDAO clientEventDAO;
private final SEBClientEventAdminService sebClientEventAdminService;
@ -138,6 +147,49 @@ public class ClientEventController extends ReadonlyEntityController<ClientEvent,
.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
public Collection<EntityDependency> getDependencies(
final String modelId,
@ -154,12 +206,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)