Merge remote-tracking branch 'origin/SEBSERV-191' into dev-1.2
Conflicts: src/main/java/ch/ethz/seb/sebserver/gbl/util/Utils.java
This commit is contained in:
commit
f1dc16b5ef
24 changed files with 1003 additions and 57 deletions
|
@ -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";
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -68,6 +68,10 @@ public class ClientEvent implements Entity, IndicatorValueHolder {
|
|||
}
|
||||
}
|
||||
|
||||
public enum ExportType {
|
||||
CSV
|
||||
}
|
||||
|
||||
@JsonProperty(Domain.CLIENT_EVENT.ATTR_ID)
|
||||
public final Long id;
|
||||
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -674,4 +674,11 @@ public final class Utils {
|
|||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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),
|
||||
|
||||
;
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue