SEBSERV-46 implementation back-end and part of front-end

This commit is contained in:
anhefti 2019-10-03 16:44:27 +02:00
parent 09b326fdfe
commit 8c8a0944cb
30 changed files with 1341 additions and 107 deletions

View file

@ -60,6 +60,15 @@ public final class Constants {
public static final String XML_DICT_END = public static final String XML_DICT_END =
"</dict>"; "</dict>";
public static final String XML_PLIST_NAME = "plist";
public static final String XML_PLIST_DICT_NAME = "dict";
public static final String XML_PLIST_ARRAY_NAME = "array";
public static final String XML_PLIST_KEY_NAME = "key";
public static final String XML_PLIST_BOOLEAN_TRUE = "true";
public static final String XML_PLIST_BOOLEAN_FALSE = "false";
public static final String XML_PLIST_STRING = "string";
public static final String XML_PLIST_INTEGER = "integer";
public static final String OAUTH2_GRANT_TYPE_PASSWORD = "password"; public static final String OAUTH2_GRANT_TYPE_PASSWORD = "password";
public static final String OAUTH2_GRANT_TYPE_REFRESH_TOKEN = "refresh_token"; public static final String OAUTH2_GRANT_TYPE_REFRESH_TOKEN = "refresh_token";
public static final String OAUTH2_GRANT_TYPE_CLIENT_CREDENTIALS = "client_credentials"; public static final String OAUTH2_GRANT_TYPE_CLIENT_CREDENTIALS = "client_credentials";

View file

@ -16,6 +16,7 @@ public final class API {
ACTIVATE; ACTIVATE;
} }
public static final String SEB_FILE_EXTENSION = "seb";
public static final String PARAM_LOGO_IMAGE = "logoImageBase64"; public static final String PARAM_LOGO_IMAGE = "logoImageBase64";
public static final String PARAM_INSTITUTION_ID = "institutionId"; public static final String PARAM_INSTITUTION_ID = "institutionId";
public static final String PARAM_MODEL_ID = "modelId"; public static final String PARAM_MODEL_ID = "modelId";
@ -119,6 +120,7 @@ public final class API {
public static final String CONFIGURATION_TABLE_VALUE_PATH_SEGMENT = "/table"; public static final String CONFIGURATION_TABLE_VALUE_PATH_SEGMENT = "/table";
public static final String CONFIGURATION_ATTRIBUTE_ENDPOINT = "/configuration_attribute"; public static final String CONFIGURATION_ATTRIBUTE_ENDPOINT = "/configuration_attribute";
public static final String CONFIGURATION_PLAIN_XML_DOWNLOAD_PATH_SEGMENT = "/downloadxml"; public static final String CONFIGURATION_PLAIN_XML_DOWNLOAD_PATH_SEGMENT = "/downloadxml";
public static final String CONFIGURATION_IMPORT_PATH_SEGMENT = "/import";
public static final String ORIENTATION_ENDPOINT = "/orientation"; public static final String ORIENTATION_ENDPOINT = "/orientation";
public static final String VIEW_ENDPOINT = ORIENTATION_ENDPOINT + "/view"; public static final String VIEW_ENDPOINT = ORIENTATION_ENDPOINT + "/view";

View file

@ -46,6 +46,8 @@ public final class Utils {
public static final Predicate<?> TRUE_PREDICATE = v -> true; public static final Predicate<?> TRUE_PREDICATE = v -> true;
public static final Predicate<?> FALSE_PREDICATE = v -> false; public static final Predicate<?> FALSE_PREDICATE = v -> false;
public static final Runnable EMPTY_EXECUTION = () -> {
};
private static final Logger log = LoggerFactory.getLogger(Utils.class); private static final Logger log = LoggerFactory.getLogger(Utils.class);

View file

@ -8,12 +8,15 @@
package ch.ethz.seb.sebserver.gui.content; package ch.ethz.seb.sebserver.gui.content;
import java.io.InputStream;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Supplier;
import org.eclipse.rap.rwt.RWT; import org.eclipse.rap.rwt.RWT;
import org.eclipse.rap.rwt.client.service.UrlLauncher; import org.eclipse.rap.rwt.client.service.UrlLauncher;
import org.eclipse.swt.SWT; import org.eclipse.swt.SWT;
import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Text; import org.eclipse.swt.widgets.Text;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -29,11 +32,14 @@ import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode.ConfigurationType; import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode.ConfigurationType;
import ch.ethz.seb.sebserver.gbl.model.user.UserInfo; import ch.ethz.seb.sebserver.gbl.model.user.UserInfo;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition; import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition;
import ch.ethz.seb.sebserver.gui.form.Form;
import ch.ethz.seb.sebserver.gui.form.FormBuilder; import ch.ethz.seb.sebserver.gui.form.FormBuilder;
import ch.ethz.seb.sebserver.gui.form.FormHandle; import ch.ethz.seb.sebserver.gui.form.FormHandle;
import ch.ethz.seb.sebserver.gui.service.ResourceService; import ch.ethz.seb.sebserver.gui.service.ResourceService;
import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
import ch.ethz.seb.sebserver.gui.service.page.ModalInputDialogComposer;
import ch.ethz.seb.sebserver.gui.service.page.PageContext; import ch.ethz.seb.sebserver.gui.service.page.PageContext;
import ch.ethz.seb.sebserver.gui.service.page.PageService; import ch.ethz.seb.sebserver.gui.service.page.PageService;
import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer; import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer;
@ -48,6 +54,7 @@ import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.examconfig.Ne
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.examconfig.SaveExamConfig; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.examconfig.SaveExamConfig;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser; import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser.EntityGrantCheck; import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser.EntityGrantCheck;
import ch.ethz.seb.sebserver.gui.widget.FileUploadSelection;
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory; import ch.ethz.seb.sebserver.gui.widget.WidgetFactory;
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory.CustomVariant; import ch.ethz.seb.sebserver.gui.widget.WidgetFactory.CustomVariant;
@ -58,6 +65,8 @@ public class SebExamConfigPropForm implements TemplateComposer {
private static final Logger log = LoggerFactory.getLogger(SebExamConfigPropForm.class); private static final Logger log = LoggerFactory.getLogger(SebExamConfigPropForm.class);
private static final String PASSWORD_ATTR_NAME = "importFilePassword";
private static final String IMPORT_FILE_ATTR_NAME = "importFile";
private static final LocTextKey FORM_TITLE_NEW = private static final LocTextKey FORM_TITLE_NEW =
new LocTextKey("sebserver.examconfig.form.title.new"); new LocTextKey("sebserver.examconfig.form.title.new");
private static final LocTextKey FORM_TITLE = private static final LocTextKey FORM_TITLE =
@ -68,7 +77,12 @@ public class SebExamConfigPropForm implements TemplateComposer {
new LocTextKey("sebserver.examconfig.form.description"); new LocTextKey("sebserver.examconfig.form.description");
private static final LocTextKey FORM_STATUS_TEXT_KEY = private static final LocTextKey FORM_STATUS_TEXT_KEY =
new LocTextKey("sebserver.examconfig.form.status"); new LocTextKey("sebserver.examconfig.form.status");
private static final LocTextKey FORM_IMPORT_TEXT_KEY =
new LocTextKey("sebserver.examconfig.action.import-config");
private static final LocTextKey FORM_IMPORT_SELECT_TEXT_KEY =
new LocTextKey("sebserver.examconfig.action.import-file-select");
private static final LocTextKey FORM_IMPORT_PASSWORD_TEXT_KEY =
new LocTextKey("sebserver.examconfig.action.import-file-password");
private static final LocTextKey CONFIG_KEY_TITLE_TEXT_KEY = private static final LocTextKey CONFIG_KEY_TITLE_TEXT_KEY =
new LocTextKey("sebserver.examconfig.form.config-key.title"); new LocTextKey("sebserver.examconfig.form.config-key.title");
@ -201,6 +215,12 @@ public class SebExamConfigPropForm implements TemplateComposer {
.noEventPropagation() .noEventPropagation()
.publishIf(() -> readGrant && isReadonly) .publishIf(() -> readGrant && isReadonly)
.newAction(ActionDefinition.SEB_EXAM_CONFIG_IMPORT_CONFIG)
.withEntityKey(entityKey)
.withExec(SebExamConfigPropForm.importConfigFunction(this.pageService))
.noEventPropagation()
.publishIf(() -> readGrant && isReadonly)
.newAction(ActionDefinition.SEB_EXAM_CONFIG_SAVE) .newAction(ActionDefinition.SEB_EXAM_CONFIG_SAVE)
.withEntityKey(entityKey) .withEntityKey(entityKey)
.withExec(formHandle::processFormSave) .withExec(formHandle::processFormSave)
@ -247,4 +267,71 @@ public class SebExamConfigPropForm implements TemplateComposer {
}; };
} }
private static Function<PageAction, PageAction> importConfigFunction(final PageService pageService) {
return action -> {
final ModalInputDialog<FormHandle<ConfigurationNode>> dialog =
new ModalInputDialog<FormHandle<ConfigurationNode>>(
action.pageContext().getParent().getShell(),
pageService.getWidgetFactory())
.setDialogWidth(600);
final ImportFormBuilder importFormBuilder = new ImportFormBuilder(
pageService,
action.pageContext());
dialog.open(
FORM_IMPORT_TEXT_KEY,
SebExamConfigPropForm::doImport,
Utils.EMPTY_EXECUTION,
importFormBuilder);
return action;
};
}
// TODO
private static final void doImport(final FormHandle<ConfigurationNode> formHandle) {
final Form form = formHandle.getForm();
final EntityKey entityKey = formHandle.getContext().getEntityKey();
final Control fieldControl = form.getFieldControl(IMPORT_FILE_ATTR_NAME);
if (fieldControl != null && fieldControl instanceof FileUploadSelection) {
final InputStream inputStream = ((FileUploadSelection) fieldControl).getInputStream();
if (inputStream != null) {
// TODO
}
}
}
private static final class ImportFormBuilder implements ModalInputDialogComposer<FormHandle<ConfigurationNode>> {
private final PageService pageService;
private final PageContext pageContext;
protected ImportFormBuilder(final PageService pageService, final PageContext pageContext) {
this.pageService = pageService;
this.pageContext = pageContext;
}
@Override
public Supplier<FormHandle<ConfigurationNode>> compose(final Composite parent) {
final FormHandle<ConfigurationNode> formHandle = this.pageService.formBuilder(
this.pageContext.copyOf(parent), 4)
.readonly(false)
.addField(FormBuilder.fileUpload(
IMPORT_FILE_ATTR_NAME,
FORM_IMPORT_SELECT_TEXT_KEY,
null,
API.SEB_FILE_EXTENSION))
.addField(FormBuilder.text(
PASSWORD_ATTR_NAME,
FORM_IMPORT_PASSWORD_TEXT_KEY,
"").asPasswordField())
.build();
return () -> formHandle;
}
}
} }

View file

@ -393,6 +393,10 @@ public enum ActionDefinition {
new LocTextKey("sebserver.examconfig.action.get-config-key"), new LocTextKey("sebserver.examconfig.action.get-config-key"),
ImageIcon.SECURE, ImageIcon.SECURE,
ActionCategory.FORM), ActionCategory.FORM),
SEB_EXAM_CONFIG_IMPORT_CONFIG(
new LocTextKey("sebserver.examconfig.action.import-config"),
ImageIcon.IMPORT,
ActionCategory.FORM),
SEB_EXAM_CONFIG_MODIFY_FROM_LIST( SEB_EXAM_CONFIG_MODIFY_FROM_LIST(
new LocTextKey("sebserver.examconfig.action.list.modify"), new LocTextKey("sebserver.examconfig.action.list.modify"),

View file

@ -0,0 +1,58 @@
/*
* Copyright (c) 2019 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.form;
import java.util.Collection;
import org.eclipse.swt.SWT;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Label;
import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
import ch.ethz.seb.sebserver.gui.widget.FileUploadSelection;
public class FileUploadFieldBuilder extends FieldBuilder<String> {
private final Collection<String> supportedFiles;
FileUploadFieldBuilder(
final String name,
final LocTextKey label,
final String value,
final Collection<String> supportedFiles) {
super(name, label, value);
this.supportedFiles = supportedFiles;
}
@Override
void build(final FormBuilder builder) {
final Label lab = builder.labelLocalized(
builder.formParent,
this.label,
this.defaultLabel,
1);
final Composite fieldGrid = Form.createFieldGrid(builder.formParent, this.spanInput);
final FileUploadSelection fileUpload = builder.widgetFactory.fileUploadSelection(
fieldGrid,
builder.readonly || this.readonly,
this.supportedFiles);
final GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, false);
fileUpload.setLayoutData(gridData);
fileUpload.setFileName(this.value);
final Label errorLabel = Form.createErrorLabel(fieldGrid);
builder.form.putField(this.name, lab, fileUpload, errorLabel);
builder.setFieldVisible(this.visible, this.name);
}
}

View file

@ -40,7 +40,8 @@ import ch.ethz.seb.sebserver.gbl.model.exam.Indicator.Threshold;
import ch.ethz.seb.sebserver.gbl.util.Tuple; import ch.ethz.seb.sebserver.gbl.util.Tuple;
import ch.ethz.seb.sebserver.gbl.util.Utils; import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.FormBinding; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.FormBinding;
import ch.ethz.seb.sebserver.gui.widget.ImageUpload; import ch.ethz.seb.sebserver.gui.widget.FileUploadSelection;
import ch.ethz.seb.sebserver.gui.widget.ImageUploadSelection;
import ch.ethz.seb.sebserver.gui.widget.Selection; import ch.ethz.seb.sebserver.gui.widget.Selection;
import ch.ethz.seb.sebserver.gui.widget.Selection.Type; import ch.ethz.seb.sebserver.gui.widget.Selection.Type;
import ch.ethz.seb.sebserver.gui.widget.ThresholdList; import ch.ethz.seb.sebserver.gui.widget.ThresholdList;
@ -137,12 +138,19 @@ public final class Form implements FormBinding {
this.formFields.add(name, createAccessor(label, field, errorLabel)); this.formFields.add(name, createAccessor(label, field, errorLabel));
} }
void putField(final String name, final Label label, final ImageUpload imageUpload, final Label errorLabel) { void putField(final String name, final Label label, final ImageUploadSelection imageUpload,
final Label errorLabel) {
final FormFieldAccessor createAccessor = createAccessor(label, imageUpload, errorLabel); final FormFieldAccessor createAccessor = createAccessor(label, imageUpload, errorLabel);
imageUpload.setErrorHandler(createAccessor::setError); imageUpload.setErrorHandler(createAccessor::setError);
this.formFields.add(name, createAccessor); this.formFields.add(name, createAccessor);
} }
void putField(final String name, final Label label, final FileUploadSelection fileUpload, final Label errorLabel) {
final FormFieldAccessor createAccessor = createAccessor(label, fileUpload, errorLabel);
fileUpload.setErrorHandler(createAccessor::setError);
this.formFields.add(name, createAccessor);
}
public String getFieldValue(final String attributeName) { public String getFieldValue(final String attributeName) {
final FormFieldAccessor fieldAccessor = this.formFields.getFirst(attributeName); final FormFieldAccessor fieldAccessor = this.formFields.getFirst(attributeName);
if (fieldAccessor == null) { if (fieldAccessor == null) {
@ -152,6 +160,15 @@ public final class Form implements FormBinding {
return fieldAccessor.getStringValue(); return fieldAccessor.getStringValue();
} }
public Control getFieldControl(final String attributeName) {
final FormFieldAccessor fieldAccessor = this.formFields.getFirst(attributeName);
if (fieldAccessor == null) {
return null;
}
return fieldAccessor.control;
}
public void setFieldValue(final String attributeName, final String attributeValue) { public void setFieldValue(final String attributeName, final String attributeValue) {
final FormFieldAccessor fieldAccessor = this.formFields.getFirst(attributeName); final FormFieldAccessor fieldAccessor = this.formFields.getFirst(attributeName);
if (fieldAccessor == null) { if (fieldAccessor == null) {
@ -291,11 +308,16 @@ public final class Form implements FormBinding {
} }
}; };
} }
private FormFieldAccessor createAccessor(final Label label, final ImageUpload imageUpload, final Label errorLabel) { private FormFieldAccessor createAccessor(final Label label, final ImageUploadSelection imageUpload, final Label errorLabel) {
return new FormFieldAccessor(label, imageUpload, errorLabel) { return new FormFieldAccessor(label, imageUpload, errorLabel) {
@Override public String getStringValue() { return imageUpload.getImageBase64(); } @Override public String getStringValue() { return imageUpload.getImageBase64(); }
}; };
} }
private FormFieldAccessor createAccessor(final Label label, final FileUploadSelection fileUpload, final Label errorLabel) {
return new FormFieldAccessor(label, fileUpload, errorLabel) {
@Override public String getStringValue() { return fileUpload.getFileName(); }
};
}
//@formatter:on //@formatter:on
/* /*

View file

@ -8,7 +8,9 @@
package ch.ethz.seb.sebserver.gui.form; package ch.ethz.seb.sebserver.gui.form;
import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.function.BooleanSupplier; import java.util.function.BooleanSupplier;
import java.util.function.Supplier; import java.util.function.Supplier;
@ -66,9 +68,9 @@ public class FormBuilder {
this.pageContext = pageContext; this.pageContext = pageContext;
this.form = new Form(pageService.getJSONMapper()); this.form = new Form(pageService.getJSONMapper());
this.formParent = this.widgetFactory this.formParent = this.widgetFactory.formGrid(
.formGrid(pageContext.getParent(), rows); pageContext.getParent(),
this.formParent.setData("TEST"); rows);
} }
public FormBuilder readonly(final boolean readonly) { public FormBuilder readonly(final boolean readonly) {
@ -252,6 +254,19 @@ public class FormBuilder {
return new ImageUploadFieldBuilder(name, label, value); return new ImageUploadFieldBuilder(name, label, value);
} }
public static FileUploadFieldBuilder fileUpload(
final String name,
final LocTextKey label,
final String value,
final String... supportedFiles) {
return new FileUploadFieldBuilder(
name,
label,
value,
(supportedFiles != null) ? Arrays.asList(supportedFiles) : Collections.emptyList());
}
Label labelLocalized( Label labelLocalized(
final Composite parent, final Composite parent,
final LocTextKey locTextKey, final LocTextKey locTextKey,

View file

@ -54,6 +54,10 @@ public class FormHandle<T extends Entity> {
this.i18nSupport = pageService.getI18nSupport(); this.i18nSupport = pageService.getI18nSupport();
} }
public PageContext getContext() {
return this.pageContext;
}
public FormBinding getFormBinding() { public FormBinding getFormBinding() {
return this.form; return this.form;
} }

View file

@ -14,7 +14,7 @@ import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Label;
import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
import ch.ethz.seb.sebserver.gui.widget.ImageUpload; import ch.ethz.seb.sebserver.gui.widget.ImageUploadSelection;
public final class ImageUploadFieldBuilder extends FieldBuilder<String> { public final class ImageUploadFieldBuilder extends FieldBuilder<String> {
@ -45,7 +45,7 @@ public final class ImageUploadFieldBuilder extends FieldBuilder<String> {
1); 1);
final Composite fieldGrid = Form.createFieldGrid(builder.formParent, this.spanInput); final Composite fieldGrid = Form.createFieldGrid(builder.formParent, this.spanInput);
final ImageUpload imageUpload = builder.widgetFactory.imageUploadLocalized( final ImageUploadSelection imageUpload = builder.widgetFactory.imageUploadLocalized(
fieldGrid, fieldGrid,
new LocTextKey("sebserver.overall.upload"), new LocTextKey("sebserver.overall.upload"),
builder.readonly || this.readonly, builder.readonly || this.readonly,

View file

@ -23,7 +23,7 @@ import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeItem; import org.eclipse.swt.widgets.TreeItem;
import ch.ethz.seb.sebserver.gui.service.page.PageContext; import ch.ethz.seb.sebserver.gui.service.page.PageContext;
import ch.ethz.seb.sebserver.gui.widget.ImageUpload; import ch.ethz.seb.sebserver.gui.widget.ImageUploadSelection;
public interface PolyglotPageService { public interface PolyglotPageService {
@ -49,7 +49,7 @@ public interface PolyglotPageService {
* @param locale the Locale to set */ * @param locale the Locale to set */
void setPageLocale(Composite root, Locale locale); void setPageLocale(Composite root, Locale locale);
void injectI18n(ImageUpload imageUpload, LocTextKey locTextKey); void injectI18n(ImageUploadSelection imageUpload, LocTextKey locTextKey);
void injectI18n(Label label, LocTextKey locTextKey); void injectI18n(Label label, LocTextKey locTextKey);

View file

@ -35,7 +35,7 @@ import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
import ch.ethz.seb.sebserver.gui.service.i18n.PolyglotPageService; import ch.ethz.seb.sebserver.gui.service.i18n.PolyglotPageService;
import ch.ethz.seb.sebserver.gui.service.page.ComposerService; import ch.ethz.seb.sebserver.gui.service.page.ComposerService;
import ch.ethz.seb.sebserver.gui.service.page.PageContext; import ch.ethz.seb.sebserver.gui.service.page.PageContext;
import ch.ethz.seb.sebserver.gui.widget.ImageUpload; import ch.ethz.seb.sebserver.gui.widget.ImageUploadSelection;
/** Service that supports page language change on the fly */ /** Service that supports page language change on the fly */
@Lazy @Lazy
@ -72,8 +72,8 @@ public final class PolyglotPageServiceImpl implements PolyglotPageService {
} }
@Override @Override
public void injectI18n(final ImageUpload imageUpload, final LocTextKey locTextKey) { public void injectI18n(final ImageUploadSelection imageUpload, final LocTextKey locTextKey) {
final Consumer<ImageUpload> imageUploadFunction = iu -> { final Consumer<ImageUploadSelection> imageUploadFunction = iu -> {
if (locTextKey != null) { if (locTextKey != null) {
iu.setSelectionText(this.i18nSupport.getText(locTextKey)); iu.setSelectionText(this.i18nSupport.getText(locTextKey));
} }

View file

@ -9,6 +9,7 @@
package ch.ethz.seb.sebserver.gui.service.remote.webservice.api; package ch.ethz.seb.sebserver.gui.service.remote.webservice.api;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@ -185,6 +186,8 @@ public abstract class RestCall<T> {
private UriComponentsBuilder uriComponentsBuilder; private UriComponentsBuilder uriComponentsBuilder;
private final HttpHeaders httpHeaders; private final HttpHeaders httpHeaders;
private String body = null; private String body = null;
private InputStream streamingBody = null;
private final MultiValueMap<String, String> queryParams; private final MultiValueMap<String, String> queryParams;
private final Map<String, String> uriVariables; private final Map<String, String> uriVariables;
@ -247,6 +250,11 @@ public abstract class RestCall<T> {
return this; return this;
} }
if (body instanceof InputStream) {
this.streamingBody = (InputStream) body;
return this;
}
try { try {
this.body = RestCall.this.jsonMapper.writeValueAsString(body); this.body = RestCall.this.jsonMapper.writeValueAsString(body);
} catch (final JsonProcessingException e) { } catch (final JsonProcessingException e) {
@ -325,7 +333,9 @@ public abstract class RestCall<T> {
} }
public HttpEntity<?> buildRequestEntity() { public HttpEntity<?> buildRequestEntity() {
if (this.body != null) { if (this.streamingBody != null) {
return new HttpEntity<>(this.streamingBody, this.httpHeaders);
} else if (this.body != null) {
return new HttpEntity<>(this.body, this.httpHeaders); return new HttpEntity<>(this.body, this.httpHeaders);
} else { } else {
return new HttpEntity<>(this.httpHeaders); return new HttpEntity<>(this.httpHeaders);

View file

@ -0,0 +1,42 @@
/*
* Copyright (c) 2019 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.seb.examconfig;
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.model.sebconfig.Configuration;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
@Lazy
@Component
@GuiProfile
public class ImportExamConfig extends RestCall<Configuration> {
public ImportExamConfig() {
super(new TypeKey<>(
CallType.UNDEFINED,
EntityType.CONFIGURATION,
new TypeReference<Configuration>() {
}),
HttpMethod.POST,
MediaType.APPLICATION_FORM_URLENCODED,
API.CONFIGURATION_NODE_ENDPOINT
+ API.MODEL_ID_VAR_PATH_SEGMENT
+ API.CONFIGURATION_IMPORT_PATH_SEGMENT);
}
}

View file

@ -0,0 +1,153 @@
/*
* Copyright (c) 2019 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.widget;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import org.eclipse.rap.fileupload.FileDetails;
import org.eclipse.rap.fileupload.FileUploadHandler;
import org.eclipse.rap.fileupload.FileUploadReceiver;
import org.eclipse.rap.rwt.widgets.FileUpload;
import org.eclipse.swt.SWT;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Label;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gui.service.i18n.I18nSupport;
import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
import ch.ethz.seb.sebserver.gui.service.push.ServerPushService;
public class FileUploadSelection extends Composite {
private static final long serialVersionUID = 5800153475027387363L;
private static final LocTextKey PLEASE_SELECT_TEXT =
new LocTextKey("sebserver.overall.upload");
private final I18nSupport i18nSupport;
private final ServerPushService serverPushService;
private final List<String> supportedFileExtensions = new ArrayList<>();
private final boolean readonly;
private final FileUpload fileUpload;
private final Label fileName;
private Consumer<String> errorHandler;
private InputStream inputStream;
public FileUploadSelection(
final Composite parent,
final ServerPushService serverPushService,
final I18nSupport i18nSupport,
final boolean readonly) {
super(parent, SWT.NONE);
final GridLayout gridLayout = new GridLayout(2, false);
gridLayout.horizontalSpacing = 0;
gridLayout.marginHeight = 0;
gridLayout.marginWidth = 0;
gridLayout.verticalSpacing = 0;
super.setLayout(gridLayout);
this.i18nSupport = i18nSupport;
this.serverPushService = serverPushService;
this.readonly = readonly;
if (readonly) {
this.fileName = new Label(this, SWT.NONE);
this.fileName.setText(i18nSupport.getText(PLEASE_SELECT_TEXT));
this.fileName.setLayoutData(new GridData());
this.fileUpload = null;
} else {
this.fileUpload = new FileUpload(this, SWT.NONE);
this.fileUpload.setImage(WidgetFactory.ImageIcon.IMPORT.getImage(parent.getDisplay()));
this.fileUpload.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false));
this.fileUpload.setToolTipText(this.i18nSupport.getText(PLEASE_SELECT_TEXT));
final FileUploadHandler uploadHandler = new FileUploadHandler(new InputReceiver());
this.fileUpload.addListener(SWT.Selection, event -> {
final String fileName = FileUploadSelection.this.fileUpload.getFileName();
if (fileName == null || !fileSupported(fileName)) {
if (FileUploadSelection.this.errorHandler != null) {
final String text = i18nSupport.getText(new LocTextKey(
"sebserver.overall.upload.unsupported.file",
this.supportedFileExtensions.toString()),
"Unsupported image file type selected");
FileUploadSelection.this.errorHandler.accept(text);
}
return;
}
FileUploadSelection.this.fileUpload.submit(uploadHandler.getUploadUrl());
});
this.fileName = new Label(this, SWT.NONE);
this.fileName.setText(i18nSupport.getText(PLEASE_SELECT_TEXT));
this.fileName.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, true, false));
}
}
public String getFileName() {
if (this.fileName != null) {
return this.fileName.getText();
}
return Constants.EMPTY_NOTE;
}
public void setFileName(final String fileName) {
if (this.fileName != null && fileName != null) {
this.fileName.setText(fileName);
}
}
public InputStream getInputStream() {
return this.inputStream;
}
@Override
public void update() {
if (this.inputStream != null) {
this.fileName.setText(this.i18nSupport.getText(PLEASE_SELECT_TEXT));
}
if (!this.readonly) {
this.fileUpload.setToolTipText(this.i18nSupport.getText(PLEASE_SELECT_TEXT));
}
}
public FileUploadSelection setErrorHandler(final Consumer<String> errorHandler) {
this.errorHandler = errorHandler;
return this;
}
public FileUploadSelection withSupportFor(final String fileExtension) {
this.supportedFileExtensions.add(fileExtension);
return this;
}
private boolean fileSupported(final String fileName) {
return this.supportedFileExtensions
.stream()
.filter(fileType -> fileName.toUpperCase().endsWith(fileType.toUpperCase()))
.findFirst()
.isPresent();
}
private final class InputReceiver extends FileUploadReceiver {
@Override
public void receive(final InputStream stream, final FileDetails details) throws IOException {
FileUploadSelection.this.inputStream = stream;
}
}
}

View file

@ -28,8 +28,6 @@ import org.eclipse.rap.fileupload.FileUploadReceiver;
import org.eclipse.rap.rwt.RWT; import org.eclipse.rap.rwt.RWT;
import org.eclipse.rap.rwt.widgets.FileUpload; import org.eclipse.rap.rwt.widgets.FileUpload;
import org.eclipse.swt.SWT; import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.ImageData; import org.eclipse.swt.graphics.ImageData;
import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.graphics.Rectangle;
@ -43,10 +41,10 @@ import ch.ethz.seb.sebserver.gui.service.i18n.I18nSupport;
import ch.ethz.seb.sebserver.gui.service.push.ServerPushContext; import ch.ethz.seb.sebserver.gui.service.push.ServerPushContext;
import ch.ethz.seb.sebserver.gui.service.push.ServerPushService; import ch.ethz.seb.sebserver.gui.service.push.ServerPushService;
public final class ImageUpload extends Composite { public final class ImageUploadSelection extends Composite {
private static final long serialVersionUID = 368264811155804533L; private static final long serialVersionUID = 368264811155804533L;
private static final Logger log = LoggerFactory.getLogger(ImageUpload.class); private static final Logger log = LoggerFactory.getLogger(ImageUploadSelection.class);
public static final Set<String> SUPPORTED_IMAGE_FILES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList( public static final Set<String> SUPPORTED_IMAGE_FILES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
".png", ".png",
@ -65,7 +63,7 @@ public final class ImageUpload extends Composite {
private boolean loadNewImage = false; private boolean loadNewImage = false;
private boolean imageLoaded = false; private boolean imageLoaded = false;
ImageUpload( ImageUploadSelection(
final Composite parent, final Composite parent,
final ServerPushService serverPushService, final ServerPushService serverPushService,
final I18nSupport i18nSupport, final I18nSupport i18nSupport,
@ -74,7 +72,12 @@ public final class ImageUpload extends Composite {
final int maxHeight) { final int maxHeight) {
super(parent, SWT.NONE); super(parent, SWT.NONE);
super.setLayout(new GridLayout(1, false)); final GridLayout gridLayout = new GridLayout(1, false);
gridLayout.horizontalSpacing = 0;
gridLayout.marginHeight = 0;
gridLayout.marginWidth = 0;
gridLayout.verticalSpacing = 0;
super.setLayout(gridLayout);
this.serverPushService = serverPushService; this.serverPushService = serverPushService;
this.maxWidth = maxWidth; this.maxWidth = maxWidth;
@ -85,55 +88,29 @@ public final class ImageUpload extends Composite {
this.fileUpload.setImage(WidgetFactory.ImageIcon.IMPORT.getImage(parent.getDisplay())); this.fileUpload.setImage(WidgetFactory.ImageIcon.IMPORT.getImage(parent.getDisplay()));
this.fileUpload.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false)); this.fileUpload.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false));
final FileUploadHandler uploadHandler = new FileUploadHandler(new FileUploadReceiver() { final FileUploadHandler uploadHandler = new FileUploadHandler(new ImageReceiver());
this.fileUpload.addListener(SWT.Selection, event -> {
@Override final String fileName = ImageUploadSelection.this.fileUpload.getFileName();
public void receive(final InputStream stream, final FileDetails details) throws IOException {
try {
final String contentType = details.getContentType();
if (contentType != null && contentType.startsWith("image")) {
ImageUpload.this.imageBase64 = Base64.getEncoder()
.encodeToString(IOUtils.toByteArray(stream));
}
} catch (final Exception e) {
log.error("Error while trying to upload image", e);
} finally {
ImageUpload.this.imageLoaded = true;
stream.close();
}
}
});
this.fileUpload.addSelectionListener(new SelectionAdapter() {
private static final long serialVersionUID = -6776734104137568801L;
@Override
public void widgetSelected(final SelectionEvent event) {
final String fileName = ImageUpload.this.fileUpload.getFileName();
if (fileName == null || !fileSupported(fileName)) { if (fileName == null || !fileSupported(fileName)) {
if (ImageUpload.this.errorHandler != null) { if (ImageUploadSelection.this.errorHandler != null) {
final String text = i18nSupport.getText( final String text = i18nSupport.getText(
"sebserver.institution.form.logoImage.unsupportedFileType", "sebserver.institution.form.logoImage.unsupportedFileType",
"Unsupported image file type selected"); "Unsupported image file type selected");
ImageUpload.this.errorHandler.accept(text); ImageUploadSelection.this.errorHandler.accept(text);
} }
log.warn("Unsupported image file selected: {}", fileName); log.warn("Unsupported image file selected: {}", fileName);
return; return;
} }
ImageUpload.this.loadNewImage = true; ImageUploadSelection.this.loadNewImage = true;
ImageUpload.this.imageLoaded = false; ImageUploadSelection.this.imageLoaded = false;
ImageUpload.this.fileUpload.submit(uploadHandler.getUploadUrl()); ImageUploadSelection.this.fileUpload.submit(uploadHandler.getUploadUrl());
ImageUpload.this.serverPushService.runServerPush( ImageUploadSelection.this.serverPushService.runServerPush(
new ServerPushContext(ImageUpload.this, ImageUpload::uploadInProgress), new ServerPushContext(ImageUploadSelection.this, ImageUploadSelection::uploadInProgress),
200, 200,
ImageUpload::update); ImageUploadSelection::update);
}
}); });
} else { } else {
this.fileUpload = null; this.fileUpload = null;
@ -142,7 +119,6 @@ public final class ImageUpload extends Composite {
this.imageCanvas = new Composite(this, SWT.NONE); this.imageCanvas = new Composite(this, SWT.NONE);
final GridData canvas = new GridData(SWT.FILL, SWT.FILL, true, true); final GridData canvas = new GridData(SWT.FILL, SWT.FILL, true, true);
this.imageCanvas.setLayoutData(canvas); this.imageCanvas.setLayoutData(canvas);
} }
public void setErrorHandler(final Consumer<String> errorHandler) { public void setErrorHandler(final Consumer<String> errorHandler) {
@ -172,12 +148,12 @@ public final class ImageUpload extends Composite {
} }
private static final boolean uploadInProgress(final ServerPushContext context) { private static final boolean uploadInProgress(final ServerPushContext context) {
final ImageUpload imageUpload = (ImageUpload) context.getAnchor(); final ImageUploadSelection imageUpload = (ImageUploadSelection) context.getAnchor();
return imageUpload.loadNewImage && !imageUpload.imageLoaded; return imageUpload.loadNewImage && !imageUpload.imageLoaded;
} }
private static final void update(final ServerPushContext context) { private static final void update(final ServerPushContext context) {
final ImageUpload imageUpload = (ImageUpload) context.getAnchor(); final ImageUploadSelection imageUpload = (ImageUploadSelection) context.getAnchor();
if (imageUpload.imageBase64 != null if (imageUpload.imageBase64 != null
&& imageUpload.loadNewImage && imageUpload.loadNewImage
&& imageUpload.imageLoaded) { && imageUpload.imageLoaded) {
@ -195,7 +171,7 @@ public final class ImageUpload extends Composite {
} }
} }
private static void setImage(final ImageUpload imageUpload, final Base64InputStream input) { private static void setImage(final ImageUploadSelection imageUpload, final Base64InputStream input) {
imageUpload.imageCanvas.setData(RWT.CUSTOM_VARIANT, "bgLogoNoImage"); imageUpload.imageCanvas.setData(RWT.CUSTOM_VARIANT, "bgLogoNoImage");
final Image image = new Image(imageUpload.imageCanvas.getDisplay(), input); final Image image = new Image(imageUpload.imageCanvas.getDisplay(), input);
@ -218,4 +194,23 @@ public final class ImageUpload extends Composite {
.isPresent(); .isPresent();
} }
private final class ImageReceiver extends FileUploadReceiver {
@Override
public void receive(final InputStream stream, final FileDetails details) throws IOException {
try {
final String contentType = details.getContentType();
if (contentType != null && contentType.startsWith("image")) {
ImageUploadSelection.this.imageBase64 = Base64.getEncoder()
.encodeToString(IOUtils.toByteArray(stream));
}
} catch (final Exception e) {
log.error("Error while trying to upload image", e);
} finally {
ImageUploadSelection.this.imageLoaded = true;
stream.close();
}
}
}
} }

View file

@ -585,7 +585,7 @@ public class WidgetFactory {
return thresholdList; return thresholdList;
} }
public ImageUpload logoImageUploadLocalized( public ImageUploadSelection logoImageUploadLocalized(
final Composite parent, final Composite parent,
final LocTextKey locTextKey, final LocTextKey locTextKey,
final boolean readonly) { final boolean readonly) {
@ -598,14 +598,14 @@ public class WidgetFactory {
DefaultPageLayout.LOGO_IMAGE_MAX_HEIGHT); DefaultPageLayout.LOGO_IMAGE_MAX_HEIGHT);
} }
public ImageUpload imageUploadLocalized( public ImageUploadSelection imageUploadLocalized(
final Composite parent, final Composite parent,
final LocTextKey locTextKey, final LocTextKey locTextKey,
final boolean readonly, final boolean readonly,
final int maxWidth, final int maxWidth,
final int maxHeight) { final int maxHeight) {
final ImageUpload imageUpload = new ImageUpload( final ImageUploadSelection imageUpload = new ImageUploadSelection(
parent, parent,
this.serverPushService, this.serverPushService,
this.i18nSupport, this.i18nSupport,
@ -617,4 +617,17 @@ public class WidgetFactory {
return imageUpload; return imageUpload;
} }
public FileUploadSelection fileUploadSelection(
final Composite parent,
final boolean readonly,
final Collection<String> supportedFiles) {
final FileUploadSelection fileUploadSelection =
new FileUploadSelection(parent, null, this.i18nSupport, readonly);
if (supportedFiles != null) {
supportedFiles.forEach(ext -> fileUploadSelection.withSupportFor(ext));
}
return fileUploadSelection;
}
} }

View file

@ -10,9 +10,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.security.cert.Certificate; import java.util.Arrays;
import java.util.function.Function;
import java.util.function.Supplier;
import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.Async;
@ -54,6 +52,15 @@ public interface SebConfigEncryptionService {
this.header = Utils.toByteArray(headerKey); this.header = Utils.toByteArray(headerKey);
} }
public static Strategy getStrategy(final byte[] header) {
return Arrays.asList(Strategy.values())
.stream()
.filter(strategy -> Arrays.equals(strategy.header, header))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException(
"No Strategy for header: " + Utils.toString(header) + " found."));
}
} }
/** This can be used to stream incoming plain text data to encrypted cipher data output stream. /** This can be used to stream incoming plain text data to encrypted cipher data output stream.
@ -76,7 +83,6 @@ public interface SebConfigEncryptionService {
void streamDecrypted( void streamDecrypted(
final OutputStream output, final OutputStream output,
final InputStream input, final InputStream input,
Supplier<CharSequence> passwordSupplier, final SebConfigEncryptionContext context);
Function<CharSequence, Certificate> certificateStore);
} }

View file

@ -8,9 +8,11 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig; package ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig;
import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import ch.ethz.seb.sebserver.gbl.api.APIMessage.FieldValidationException; import ch.ethz.seb.sebserver.gbl.api.APIMessage.FieldValidationException;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.Configuration;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationTableValues; import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationTableValues;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationValue; import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationValue;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
@ -94,4 +96,21 @@ public interface SebExamConfigService {
* @return Result refer to the generated Config-Key or to an error if happened. */ * @return Result refer to the generated Config-Key or to an error if happened. */
Result<String> generateConfigKey(Long institutionId, Long configurationNodeId); Result<String> generateConfigKey(Long institutionId, Long configurationNodeId);
/** Imports a SEB Exam Configuration from a SEB File of the format:
* https://www.safeexambrowser.org/developer/seb-file-format.html
*
* First tries to read the file from the given input stream and detect the file format. A password
* is needed if the file is in an encrypted format.
*
* Then loads the ConfigurationNode on which the import should take place and performs a "save in histroy"
* action first to allow to make an easy rollback or even later an undo by the user.
*
* Then parses the XML and adds each attribute to the new Configuration.
*
* @param configNodeId The identifier of the configuration node on which the import should take place
* @param input The InputStream to get the SEB config file as byte-stream
* @param password A password is only needed if the file is in an encrypted format
* @return The newly created Configuration instance */
Result<Configuration> importFromXML(Long configNodeId, InputStream input, CharSequence password);
} }

View file

@ -19,12 +19,17 @@ import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.apache.tomcat.util.http.fileupload.IOUtils; import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.xml.sax.SAXException;
import ch.ethz.seb.sebserver.gbl.Constants; import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.async.AsyncServiceSpringConfig; import ch.ethz.seb.sebserver.gbl.async.AsyncServiceSpringConfig;
@ -35,6 +40,7 @@ import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationAttributeDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationAttributeDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationValueDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationValueDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.AttributeValueConverter; import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.AttributeValueConverter;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.AttributeValueConverterService; import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.AttributeValueConverterService;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ConfigurationFormat; import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ConfigurationFormat;
@ -153,6 +159,50 @@ public class ExamConfigIO {
} }
} }
/** This parses the XML from given InputStream with a SAX parser to avoid keeping the
* whole XML file in memory and keep up with the streaming approach of SEB Exam Configuration
* to avoid trouble with big SEB Exam Configuration in the future.
*
* @param in The InputString to constantly read the XML from
* @param institutionId the institionId of the import
* @param configurationId the identifier of the internal configuration to apply the imported values to */
@Async(AsyncServiceSpringConfig.EXECUTOR_BEAN_NAME)
void importPlainXML(final InputStream in, final Long institutionId, final Long configurationId) {
try {
// get all attributes and map the names to ids
final Map<String, Long> attributeMap = this.configurationAttributeDAO
.allMatching(new FilterMap())
.getOrThrow()
.stream()
.collect(Collectors.toMap(attr -> attr.name, attr -> attr.id));
// the SAX handler with a ConfigValue sink that saves the values to DB
// and a attribute-name/id mapping function with pre-created mapping
final ExamConfigImportHandler examConfigImportHandler = new ExamConfigImportHandler(
institutionId,
configurationId,
value -> this.configurationValueDAO.save(value),
attributeMap::get);
// SAX parsing
final SAXParserFactory saxParserFactory = SAXParserFactory.newInstance();
final SAXParser parser = saxParserFactory.newSAXParser();
parser.parse(in, examConfigImportHandler);
} catch (final ParserConfigurationException e) {
log.error("Unexpected error while trying to parse imported SEB Config XML: ", e);
throw new RuntimeException(e);
} catch (final SAXException e) {
log.error("Unexpected error while trying to parse imported SEB Config XML: ", e);
throw new RuntimeException(e);
} catch (final IOException e) {
log.error("Unexpected error while trying to parse imported SEB Config XML: ", e);
throw new RuntimeException(e);
} finally {
IOUtils.closeQuietly(in);
}
}
private Predicate<ConfigurationAttribute> exportFormatBasedAttributeFilter(final ConfigurationFormat format) { private Predicate<ConfigurationAttribute> exportFormatBasedAttributeFilter(final ConfigurationFormat format) {
// Filter originatorVersion according to: https://www.safeexambrowser.org/developer/seb-config-key.html // Filter originatorVersion according to: https://www.safeexambrowser.org/developer/seb-config-key.html
return attr -> !("originatorVersion".equals(attr.getName()) && format == ConfigurationFormat.JSON); return attr -> !("originatorVersion".equals(attr.getName()) && format == ConfigurationFormat.JSON);
@ -206,11 +256,6 @@ public class ExamConfigIO {
} }
} }
@Async(AsyncServiceSpringConfig.EXECUTOR_BEAN_NAME)
void importPlainXML(final InputStream in, final Long institutionId, final Long configurationNodeId) {
// TODO version 1
}
private Function<ConfigurationAttribute, ConfigurationValue> getConfigurationValueSupplier( private Function<ConfigurationAttribute, ConfigurationValue> getConfigurationValueSupplier(
final Long institutionId, final Long institutionId,
final Long configurationId) { final Long configurationId) {

View file

@ -0,0 +1,286 @@
/*
* Copyright (c) 2019 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.sebconfig.impl;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.Stack;
import java.util.function.Consumer;
import java.util.function.Function;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationValue;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.impl.ExamConfigImportHandler.PListNode.Type;
public class ExamConfigImportHandler extends DefaultHandler {
private static final Set<String> VALUE_ELEMENTS = new HashSet<>(Arrays.asList(
Constants.XML_PLIST_BOOLEAN_FALSE,
Constants.XML_PLIST_BOOLEAN_TRUE,
Constants.XML_PLIST_STRING,
Constants.XML_PLIST_INTEGER));
private final Consumer<ConfigurationValue> valueConsumer;
private final Function<String, Long> attributeNameIdResolver;
private final Long institutionId;
private final Long configId;
private final Stack<PListNode> stack = new Stack<>();
protected ExamConfigImportHandler(
final Long institutionId,
final Long configId,
final Consumer<ConfigurationValue> valueConsumer,
final Function<String, Long> attributeNameIdResolver) {
super();
this.valueConsumer = valueConsumer;
this.attributeNameIdResolver = attributeNameIdResolver;
this.institutionId = institutionId;
this.configId = configId;
}
@Override
public void startElement(
final String uri,
final String localName,
final String qName,
final Attributes attributes) throws SAXException {
final Type type = Type.getType(qName);
final PListNode top = (this.stack.isEmpty()) ? null : this.stack.peek();
switch (type) {
case PLIST:
startPList(type);
break;
case DICT:
startDict(type, top);
break;
case ARRAY:
startArray(type, top);
break;
case KEY:
startKey(type, top);
break;
case VALUE_BOOLEAN_FALSE:
case VALUE_BOOLEAN_TRUE:
case VALUE_STRING:
case VALUE_INTEGER:
startValueElement(type, top);
break;
}
}
private void startKey(final Type type, final PListNode top) {
final PListNode key = new PListNode(type);
switch (top.type) {
case DICT: {
key.listIndex = top.listIndex;
this.stack.push(key);
break;
}
default:
throw new IllegalStateException();
}
}
private void startArray(final Type type, final PListNode top) {
final PListNode array = new PListNode(type);
switch (top.type) {
case KEY: {
array.name = top.name;
array.listIndex = top.listIndex;
this.stack.pop();
this.stack.push(array);
break;
}
default:
throw new IllegalStateException();
}
}
private void startDict(final Type type, final PListNode top) {
final PListNode dict = new PListNode(type);
switch (top.type) {
case PLIST: {
this.stack.push(dict);
break;
}
case ARRAY: {
dict.name = top.name;
dict.listIndex = top.arrayCounter++;
this.stack.push(dict);
break;
}
case KEY: {
dict.name = top.name;
dict.listIndex = top.listIndex;
this.stack.pop();
this.stack.push(dict);
break;
}
default:
throw new IllegalStateException();
}
}
private void startPList(final Type type) {
if (this.stack.isEmpty()) {
this.stack.push(new PListNode(type));
} else {
throw new IllegalStateException();
}
}
private void startValueElement(final Type type, final PListNode top) {
final PListNode value = new PListNode(type);
if (top.type == Type.KEY) {
if (Type.isBooleanValue(type)) {
this.stack.pop();
value.name = top.name;
value.listIndex = top.listIndex;
value.value = type == Type.VALUE_BOOLEAN_TRUE
? Constants.XML_PLIST_BOOLEAN_TRUE
: Constants.XML_PLIST_BOOLEAN_FALSE;
this.stack.push(value);
} else {
this.stack.pop();
value.name = top.name;
value.listIndex = top.listIndex;
this.stack.push(value);
}
} else if (top.type == Type.ARRAY) {
if (Type.isBooleanValue(type)) {
value.name = top.name;
value.listIndex = top.arrayCounter++;
value.value = type == Type.VALUE_BOOLEAN_TRUE
? Constants.XML_PLIST_BOOLEAN_TRUE
: Constants.XML_PLIST_BOOLEAN_FALSE;
this.stack.push(value);
} else {
value.name = top.name;
value.listIndex = top.arrayCounter++;
this.stack.push(value);
}
}
}
@Override
public void endElement(
final String uri,
final String localName,
final String qName) throws SAXException {
final PListNode top = this.stack.peek();
if (VALUE_ELEMENTS.contains(qName)) {
if (top.type.isValueType) {
this.stack.pop();
final PListNode parent = this.stack.pop();
final PListNode grandParent = this.stack.peek();
this.stack.push(parent);
final String attrName = (parent.type == Type.DICT && grandParent.type == Type.ARRAY)
? parent.name + "." + top.name
: top.name;
this.valueConsumer.accept(new ConfigurationValue(
null,
this.institutionId,
this.configId,
this.attributeNameIdResolver.apply(attrName),
top.listIndex,
top.value));
}
} else if (!Constants.XML_PLIST_KEY_NAME.equals(qName)) {
this.stack.pop();
}
}
@Override
public void characters(
final char[] ch,
final int start,
final int length) throws SAXException {
final PListNode top = this.stack.peek();
if (top.type == Type.VALUE_STRING) {
top.value = String.valueOf(ch);
} else if (top.type == Type.VALUE_INTEGER) {
top.value = String.valueOf(ch);
} else if (top.type == Type.KEY) {
top.name = String.valueOf(ch);
}
}
final static class PListNode {
enum Type {
PLIST(false, Constants.XML_PLIST_NAME),
DICT(false, Constants.XML_PLIST_DICT_NAME),
ARRAY(false, Constants.XML_PLIST_ARRAY_NAME),
KEY(false, Constants.XML_PLIST_KEY_NAME),
VALUE_BOOLEAN_TRUE(true, Constants.XML_PLIST_BOOLEAN_TRUE),
VALUE_BOOLEAN_FALSE(true, Constants.XML_PLIST_BOOLEAN_FALSE),
VALUE_STRING(true, Constants.XML_PLIST_STRING),
VALUE_INTEGER(true, Constants.XML_PLIST_INTEGER);
private final boolean isValueType;
private final String typeName;
private Type(final boolean isValueType, final String typeName) {
this.isValueType = isValueType;
this.typeName = typeName;
}
public static boolean isBooleanValue(final Type type) {
return type == VALUE_BOOLEAN_TRUE || type == VALUE_BOOLEAN_FALSE;
}
public static Type getType(final String qName) {
return Arrays.asList(Type.values()).stream()
.filter(type -> type.typeName.equals(qName))
.findFirst()
.orElse(null);
}
}
final Type type;
String name;
int arrayCounter = 0;
int listIndex = 0;
String value;
protected PListNode(final Type type) {
this.type = type;
}
@Override
public String toString() {
final StringBuilder builder = new StringBuilder();
builder.append("PListNode [type=");
builder.append(this.type);
builder.append(", name=");
builder.append(this.name);
builder.append(", listIndex=");
builder.append(this.listIndex);
builder.append(", value=");
builder.append(this.value);
builder.append("]");
return builder.toString();
}
}
}

View file

@ -18,7 +18,6 @@ import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Map; import java.util.Map;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
@ -103,8 +102,7 @@ public final class SebConfigEncryptionServiceImpl implements SebConfigEncryption
public void streamDecrypted( public void streamDecrypted(
final OutputStream output, final OutputStream output,
final InputStream input, final InputStream input,
final Supplier<CharSequence> passwordSupplier, final SebConfigEncryptionContext context) {
final Function<CharSequence, Certificate> certificateStore) {
PipedOutputStream pout = null; PipedOutputStream pout = null;
PipedInputStream pin = null; PipedInputStream pin = null;
@ -118,11 +116,6 @@ public final class SebConfigEncryptionServiceImpl implements SebConfigEncryption
log.debug("Password decryption with strategy: {}", strategy); log.debug("Password decryption with strategy: {}", strategy);
} }
final EncryptionContext context = new EncryptionContext(
strategy,
(passwordSupplier != null) ? passwordSupplier.get() : null,
certificateStore);
getEncryptor(strategy) getEncryptor(strategy)
.getOrThrow() .getOrThrow()
.decrypt(pout, input, context); .decrypt(pout, input, context);

View file

@ -8,10 +8,14 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.impl; package ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.impl;
import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.io.PipedInputStream; import java.io.PipedInputStream;
import java.io.PipedOutputStream; import java.io.PipedOutputStream;
import java.io.SequenceInputStream;
import java.nio.ByteBuffer;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -28,6 +32,7 @@ import org.springframework.stereotype.Service;
import ch.ethz.seb.sebserver.gbl.api.APIMessage; import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.api.APIMessage.APIMessageException; import ch.ethz.seb.sebserver.gbl.api.APIMessage.APIMessageException;
import ch.ethz.seb.sebserver.gbl.api.APIMessage.FieldValidationException; import ch.ethz.seb.sebserver.gbl.api.APIMessage.FieldValidationException;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.Configuration;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationAttribute; import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationAttribute;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationTableValues; import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationTableValues;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationValue; import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationValue;
@ -35,6 +40,7 @@ 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.webservice.servicelayer.client.ClientCredentialService; import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentialService;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationAttributeDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationAttributeDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamConfigurationMapDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamConfigurationMapDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ConfigurationFormat; import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ConfigurationFormat;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ConfigurationValueValidator; import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ConfigurationValueValidator;
@ -53,6 +59,7 @@ public class SebExamConfigServiceImpl implements SebExamConfigService {
private final ExamConfigIO examConfigIO; private final ExamConfigIO examConfigIO;
private final ConfigurationAttributeDAO configurationAttributeDAO; private final ConfigurationAttributeDAO configurationAttributeDAO;
private final ConfigurationDAO configurationDAO;
private final ExamConfigurationMapDAO examConfigurationMapDAO; private final ExamConfigurationMapDAO examConfigurationMapDAO;
private final Collection<ConfigurationValueValidator> validators; private final Collection<ConfigurationValueValidator> validators;
private final ClientCredentialService clientCredentialService; private final ClientCredentialService clientCredentialService;
@ -62,6 +69,7 @@ public class SebExamConfigServiceImpl implements SebExamConfigService {
protected SebExamConfigServiceImpl( protected SebExamConfigServiceImpl(
final ExamConfigIO examConfigIO, final ExamConfigIO examConfigIO,
final ConfigurationAttributeDAO configurationAttributeDAO, final ConfigurationAttributeDAO configurationAttributeDAO,
final ConfigurationDAO configurationDAO,
final ExamConfigurationMapDAO examConfigurationMapDAO, final ExamConfigurationMapDAO examConfigurationMapDAO,
final Collection<ConfigurationValueValidator> validators, final Collection<ConfigurationValueValidator> validators,
final ClientCredentialService clientCredentialService, final ClientCredentialService clientCredentialService,
@ -70,12 +78,12 @@ public class SebExamConfigServiceImpl implements SebExamConfigService {
this.examConfigIO = examConfigIO; this.examConfigIO = examConfigIO;
this.configurationAttributeDAO = configurationAttributeDAO; this.configurationAttributeDAO = configurationAttributeDAO;
this.configurationDAO = configurationDAO;
this.examConfigurationMapDAO = examConfigurationMapDAO; this.examConfigurationMapDAO = examConfigurationMapDAO;
this.validators = validators; this.validators = validators;
this.clientCredentialService = clientCredentialService; this.clientCredentialService = clientCredentialService;
this.zipService = zipService; this.zipService = zipService;
this.sebConfigEncryptionService = sebConfigEncryptionService; this.sebConfigEncryptionService = sebConfigEncryptionService;
} }
@Override @Override
@ -248,7 +256,7 @@ public class SebExamConfigServiceImpl implements SebExamConfigService {
final Long configurationNodeId) { final Long configurationNodeId) {
if (log.isDebugEnabled()) { if (log.isDebugEnabled()) {
log.debug("Start to stream plain JSON SEB clonfiguration data for Config-Key generation"); log.debug("Start to stream plain JSON SEB Configuration data for Config-Key generation");
} }
if (log.isTraceEnabled()) { if (log.isTraceEnabled()) {
@ -288,7 +296,7 @@ public class SebExamConfigServiceImpl implements SebExamConfigService {
return Result.of(configKey); return Result.of(configKey);
} catch (final Exception e) { } catch (final Exception e) {
log.error("Error while stream plain JSON SEB clonfiguration data for Config-Key generation: ", e); log.error("Error while stream plain JSON SEB Configuration data for Config-Key generation: ", e);
return Result.ofError(e); return Result.ofError(e);
} finally { } finally {
try { try {
@ -307,11 +315,116 @@ public class SebExamConfigServiceImpl implements SebExamConfigService {
} }
if (log.isDebugEnabled()) { if (log.isDebugEnabled()) {
log.debug("Finished to stream plain JSON SEB clonfiguration data for Config-Key generation"); log.debug("Finished to stream plain JSON SEB Configuration data for Config-Key generation");
} }
} }
} }
@Override
public Result<Configuration> importFromXML(
final Long configNodeId,
final InputStream input,
final CharSequence password) {
return Result.tryCatch(() -> {
final Configuration newConfig = this.configurationDAO
.saveToHistory(configNodeId)
.getOrThrow();
try {
final byte[] header = new byte[4];
input.read(header);
final Strategy strategy = SebConfigEncryptionService.Strategy.getStrategy(header);
if (strategy == null) {
importPlainOnly(input, newConfig, header);
} else {
final InputStream cryptIn = this.unzip(input);
final PipedInputStream plainIn = new PipedInputStream();
final PipedOutputStream cryptOut = new PipedOutputStream(plainIn);
try {
this.sebConfigEncryptionService.streamDecrypted(
cryptOut,
cryptIn,
EncryptionContext.contextOf(strategy, password));
this.examConfigIO.importPlainXML(
plainIn,
newConfig.institutionId,
newConfig.id);
} finally {
IOUtils.closeQuietly(cryptIn);
IOUtils.closeQuietly(cryptOut);
IOUtils.closeQuietly(plainIn);
}
}
return newConfig;
} catch (final Exception e) {
log.error("Unexpected error while trying to import SEB Exam Configuration: ", e);
log.debug("Make an undo on the ConfigurationNode to rollback the changes");
return this.configurationDAO
.undo(configNodeId)
.getOrThrow();
}
});
}
private InputStream unzip(final InputStream input) throws Exception {
final byte[] zipHeader = new byte[4];
input.read(zipHeader);
final int zipType = ByteBuffer.wrap(zipHeader).getInt();
final boolean isZipped = zipType == 0x504B0304 || zipType == 0x504B0506 || zipType == 0x504B0708;
if (isZipped) {
final InputStream sequencedInput = new SequenceInputStream(
new ByteArrayInputStream(zipHeader),
input);
final PipedInputStream pipedIn = new PipedInputStream();
final PipedOutputStream pipedOut = new PipedOutputStream(pipedIn);
this.zipService.read(pipedOut, sequencedInput);
return pipedIn;
} else {
return new SequenceInputStream(
new ByteArrayInputStream(zipHeader),
input);
}
}
private void importPlainOnly(
final InputStream input,
final Configuration newConfig,
final byte[] header) throws IOException {
PipedInputStream plainIn = null;
PipedOutputStream out = null;
try {
plainIn = new PipedInputStream();
out = new PipedOutputStream(plainIn);
this.examConfigIO.importPlainXML(plainIn, newConfig.institutionId, newConfig.id);
out.write(header);
IOUtils.copyLarge(input, out);
IOUtils.closeQuietly(out);
} catch (final Exception e) {
log.error("Error while stream plain text SEB Configuration import data: ", e);
throw e;
} finally {
IOUtils.closeQuietly(out);
IOUtils.closeQuietly(plainIn);
}
}
private void exportPlainOnly( private void exportPlainOnly(
final ConfigurationFormat exportFormat, final ConfigurationFormat exportFormat,
final OutputStream out, final OutputStream out,
@ -319,7 +432,7 @@ public class SebExamConfigServiceImpl implements SebExamConfigService {
final Long configurationNodeId) { final Long configurationNodeId) {
if (log.isDebugEnabled()) { if (log.isDebugEnabled()) {
log.debug("Start to stream plain text SEB clonfiguration data"); log.debug("Start to stream plain text SEB Configuration data");
} }
PipedOutputStream pout = null; PipedOutputStream pout = null;
@ -337,7 +450,7 @@ public class SebExamConfigServiceImpl implements SebExamConfigService {
IOUtils.copyLarge(pin, out); IOUtils.copyLarge(pin, out);
} catch (final Exception e) { } catch (final Exception e) {
log.error("Error while stream plain text SEB clonfiguration data: ", e); log.error("Error while stream plain text SEB Configuration export data: ", e);
} finally { } finally {
try { try {
if (pin != null) { if (pin != null) {
@ -356,7 +469,7 @@ public class SebExamConfigServiceImpl implements SebExamConfigService {
} }
if (log.isDebugEnabled()) { if (log.isDebugEnabled()) {
log.debug("Finished to stream plain text SEB clonfiguration data"); log.debug("Finished to stream plain text SEB Configuration export data");
} }
} }
} }

View file

@ -11,6 +11,7 @@ package ch.ethz.seb.sebserver.webservice.weblayer.api;
import java.io.IOException; import java.io.IOException;
import javax.servlet.ServletOutputStream; import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.mybatis.dynamic.sql.SqlTable; import org.mybatis.dynamic.sql.SqlTable;
@ -19,6 +20,7 @@ import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
@ -160,4 +162,24 @@ public class ConfigurationNodeController extends EntityController<ConfigurationN
} }
} }
@RequestMapping(
path = API.MODEL_ID_VAR_PATH_SEGMENT + API.CONFIGURATION_IMPORT_PATH_SEGMENT,
method = RequestMethod.GET,
produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public Configuration importExamConfig(
@PathVariable final Long modelId,
@RequestHeader final String password,
@RequestParam(
name = API.PARAM_INSTITUTION_ID,
required = true,
defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId,
final HttpServletRequest request) throws IOException {
return this.sebExamConfigService.importFromXML(
modelId,
request.getInputStream(),
password)
.getOrThrow();
}
} }

View file

@ -7,7 +7,8 @@ sebserver.overall.help=Documentation
sebserver.overall.help.link=https://www.safeexambrowser.org/news_en.html sebserver.overall.help.link=https://www.safeexambrowser.org/news_en.html
sebserver.overall.message.leave.without.save=You have unsaved changes!\nAre you sure you want to leave the page?\The changes will be lost. sebserver.overall.message.leave.without.save=You have unsaved changes!\nAre you sure you want to leave the page?\The changes will be lost.
sebserver.overall.upload=Please Select sebserver.overall.upload=Please select a file
sebserver.overall.upload.unsupported.file=This file type is not supported. Supported files are: {0}
sebserver.overall.action.modify.cancel=Cancel sebserver.overall.action.modify.cancel=Cancel
sebserver.overall.action.modify.cancel.confirm=Are you sure you want to cancel? Modifications will be lost. sebserver.overall.action.modify.cancel.confirm=Are you sure you want to cancel? Modifications will be lost.
sebserver.overall.action.filter=Apply filter sebserver.overall.action.filter=Apply filter
@ -438,8 +439,11 @@ sebserver.examconfig.action.saveToHistory=Save In History
sebserver.examconfig.action.saveToHistory.success=Successfully saved in history sebserver.examconfig.action.saveToHistory.success=Successfully saved in history
sebserver.examconfig.action.undo=Undo sebserver.examconfig.action.undo=Undo
sebserver.examconfig.action.undo.success=Successfully reverted to last saved state sebserver.examconfig.action.undo.success=Successfully reverted to last saved state
sebserver.examconfig.action.export.plainxml=Export XML sebserver.examconfig.action.export.plainxml=Export Configuration
sebserver.examconfig.action.get-config-key=Export Config-Key sebserver.examconfig.action.get-config-key=Export Config-Key
sebserver.examconfig.action.import-config=Import Configuration
sebserver.examconfig.action.import-file-select=Import From File
sebserver.examconfig.action.import-file-password=Password
sebserver.examconfig.form.title.new=Add Exam Configuration sebserver.examconfig.form.title.new=Add Exam Configuration
sebserver.examconfig.form.title=Exam Configuration sebserver.examconfig.form.title=Exam Configuration

View file

@ -7,7 +7,7 @@ sebserver.overall.help=Documentation
sebserver.overall.help.link=https://www.safeexambrowser.org/news_en.html sebserver.overall.help.link=https://www.safeexambrowser.org/news_en.html
sebserver.overall.message.leave.without.save=You have unsaved changes!\nAre you sure you want to leave the page?\The changes will be lost. sebserver.overall.message.leave.without.save=You have unsaved changes!\nAre you sure you want to leave the page?\The changes will be lost.
sebserver.overall.upload=Please Select sebserver.overall.upload=Please select a file
sebserver.overall.action.modify.cancel=Cancel sebserver.overall.action.modify.cancel=Cancel
sebserver.overall.action.modify.cancel.confirm=Are you sure you want to cancel? Modifications will be lost. sebserver.overall.action.modify.cancel.confirm=Are you sure you want to cancel? Modifications will be lost.
sebserver.overall.action.filter=Apply filter sebserver.overall.action.filter=Apply filter

Binary file not shown.

Before

Width:  |  Height:  |  Size: 143 B

After

Width:  |  Height:  |  Size: 170 B

View file

@ -15,7 +15,7 @@ import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.junit.Test; import org.junit.jupiter.api.Test;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.AttributeType; import ch.ethz.seb.sebserver.gbl.model.sebconfig.AttributeType;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationAttribute; import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationAttribute;

View file

@ -0,0 +1,332 @@
/*
* Copyright (c) 2019 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.sebconfig.impl;
import static org.junit.Assert.*;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import org.junit.jupiter.api.Test;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationValue;
public class ExamConfigImportHandlerTest {
@Test
public void simpleStringValueTest() throws Exception {
final ValueCollector valueCollector = new ValueCollector();
final ExamConfigImportHandler candidate = new ExamConfigImportHandler(
1L,
1L,
valueCollector,
name -> Long.parseLong(String.valueOf(name.charAt(name.length() - 1))));
final String attribute = "param1";
final String value = "value1";
candidate.startElement(null, null, "plist", null);
candidate.startElement(null, null, "dict", null);
candidate.startElement(null, null, "key", null);
candidate.characters(attribute.toCharArray(), 0, attribute.length());
candidate.endElement(null, null, "key");
candidate.startElement(null, null, "string", null);
candidate.characters(value.toCharArray(), 0, value.length());
candidate.endElement(null, null, "string");
candidate.endElement(null, null, "dict");
candidate.endElement(null, null, "plist");
assertFalse(valueCollector.values.isEmpty());
final ConfigurationValue configurationValue = valueCollector.values.get(0);
assertNotNull(configurationValue);
assertTrue(1L == configurationValue.attributeId);
assertEquals("value1", configurationValue.value);
}
@Test
public void simpleIntegerValueTest() throws Exception {
final ValueCollector valueCollector = new ValueCollector();
final ExamConfigImportHandler candidate = new ExamConfigImportHandler(
1L,
1L,
valueCollector,
name -> Long.parseLong(String.valueOf(name.charAt(name.length() - 1))));
final String attribute = "param2";
final String value = "22";
candidate.startElement(null, null, "plist", null);
candidate.startElement(null, null, "dict", null);
candidate.startElement(null, null, "key", null);
candidate.characters(attribute.toCharArray(), 0, attribute.length());
candidate.endElement(null, null, "key");
candidate.startElement(null, null, "integer", null);
candidate.characters(value.toCharArray(), 0, value.length());
candidate.endElement(null, null, "integer");
candidate.endElement(null, null, "dict");
candidate.endElement(null, null, "plist");
assertFalse(valueCollector.values.isEmpty());
final ConfigurationValue configurationValue = valueCollector.values.get(0);
assertNotNull(configurationValue);
assertTrue(2L == configurationValue.attributeId);
assertEquals("22", configurationValue.value);
}
@Test
public void simpleBooleanValueTest() throws Exception {
final ValueCollector valueCollector = new ValueCollector();
final ExamConfigImportHandler candidate = new ExamConfigImportHandler(
1L,
1L,
valueCollector,
name -> Long.parseLong(String.valueOf(name.charAt(name.length() - 1))));
final String attribute = "param3";
final String value = "true";
candidate.startElement(null, null, "plist", null);
candidate.startElement(null, null, "dict", null);
candidate.startElement(null, null, "key", null);
candidate.characters(attribute.toCharArray(), 0, attribute.length());
candidate.endElement(null, null, "key");
candidate.startElement(null, null, value, null);
candidate.endElement(null, null, value);
candidate.endElement(null, null, "dict");
candidate.endElement(null, null, "plist");
assertFalse(valueCollector.values.isEmpty());
final ConfigurationValue configurationValue = valueCollector.values.get(0);
assertNotNull(configurationValue);
assertTrue(3L == configurationValue.attributeId);
assertEquals("true", configurationValue.value);
}
@Test
public void arrayOfStringValueTest() throws Exception {
final ValueCollector valueCollector = new ValueCollector();
final ExamConfigImportHandler candidate = new ExamConfigImportHandler(
1L,
1L,
valueCollector,
name -> Long.parseLong(String.valueOf(name.charAt(name.length() - 1))));
final String attribute = "array1";
final String value1 = "val1";
final String value2 = "val2";
final String value3 = "val3";
candidate.startElement(null, null, "plist", null);
candidate.startElement(null, null, "dict", null);
candidate.startElement(null, null, "key", null);
candidate.characters(attribute.toCharArray(), 0, attribute.length());
candidate.endElement(null, null, "key");
candidate.startElement(null, null, "array", null);
candidate.startElement(null, null, "string", null);
candidate.characters(value1.toCharArray(), 0, value1.length());
candidate.endElement(null, null, "string");
candidate.startElement(null, null, "string", null);
candidate.characters(value2.toCharArray(), 0, value2.length());
candidate.endElement(null, null, "string");
candidate.startElement(null, null, "string", null);
candidate.characters(value3.toCharArray(), 0, value3.length());
candidate.endElement(null, null, "string");
candidate.endElement(null, null, "array");
candidate.endElement(null, null, "dict");
candidate.endElement(null, null, "plist");
assertFalse(valueCollector.values.isEmpty());
assertTrue(valueCollector.values.size() == 3);
final ConfigurationValue configurationValue1 = valueCollector.values.get(0);
assertEquals("val1", configurationValue1.value);
assertTrue(configurationValue1.listIndex == 0);
final ConfigurationValue configurationValue2 = valueCollector.values.get(1);
assertEquals("val2", configurationValue2.value);
assertTrue(configurationValue2.listIndex == 1);
final ConfigurationValue configurationValue3 = valueCollector.values.get(2);
assertEquals("val3", configurationValue3.value);
assertTrue(configurationValue3.listIndex == 2);
}
@Test
public void dictOfValuesTest() throws Exception {
final ValueCollector valueCollector = new ValueCollector();
final List<String> attrNamesCollector = new ArrayList<>();
final Function<String, Long> attrConverter = attrName -> {
attrNamesCollector.add(attrName);
return Long.parseLong(String.valueOf(attrName.charAt(attrName.length() - 1)));
};
final ExamConfigImportHandler candidate = new ExamConfigImportHandler(
1L,
1L,
valueCollector,
attrConverter);
final String attribute = "dict1";
final String attr1 = "attr1";
final String attr2 = "attr2";
final String attr3 = "attr3";
final String value1 = "val1";
final String value2 = "2";
candidate.startElement(null, null, "plist", null);
candidate.startElement(null, null, "dict", null);
candidate.startElement(null, null, "key", null);
candidate.characters(attribute.toCharArray(), 0, attribute.length());
candidate.endElement(null, null, "key");
candidate.startElement(null, null, "dict", null);
candidate.startElement(null, null, "key", null);
candidate.characters(attr1.toCharArray(), 0, attr1.length());
candidate.endElement(null, null, "key");
candidate.startElement(null, null, "string", null);
candidate.characters(value1.toCharArray(), 0, value1.length());
candidate.endElement(null, null, "string");
candidate.startElement(null, null, "key", null);
candidate.characters(attr2.toCharArray(), 0, attr2.length());
candidate.endElement(null, null, "key");
candidate.startElement(null, null, "integer", null);
candidate.characters(value2.toCharArray(), 0, value2.length());
candidate.endElement(null, null, "integer");
candidate.startElement(null, null, "key", null);
candidate.characters(attr3.toCharArray(), 0, attr3.length());
candidate.endElement(null, null, "key");
candidate.startElement(null, null, "true", null);
candidate.endElement(null, null, "true");
candidate.endElement(null, null, "dict");
candidate.endElement(null, null, "dict");
candidate.endElement(null, null, "plist");
assertFalse(valueCollector.values.isEmpty());
assertTrue(valueCollector.values.size() == 3);
assertEquals(
"[ConfigurationValue [id=null, institutionId=1, configurationId=1, attributeId=1, listIndex=0, value=val1], "
+ "ConfigurationValue [id=null, institutionId=1, configurationId=1, attributeId=2, listIndex=0, value=2], "
+ "ConfigurationValue [id=null, institutionId=1, configurationId=1, attributeId=3, listIndex=0, value=true]]",
valueCollector.values.toString());
assertEquals(
"[attr1, attr2, attr3]",
attrNamesCollector.toString());
}
@Test
public void arrayOfDictOfValuesTest() throws Exception {
final ValueCollector valueCollector = new ValueCollector();
final List<String> attrNamesCollector = new ArrayList<>();
final Function<String, Long> attrConverter = attrName -> {
attrNamesCollector.add(attrName);
return Long.parseLong(String.valueOf(attrName.charAt(attrName.length() - 1)));
};
final ExamConfigImportHandler candidate = new ExamConfigImportHandler(
1L,
1L,
valueCollector,
attrConverter);
final String attribute = "attribute";
final String attr1 = "attr1";
final String attr2 = "attr2";
final String attr3 = "attr3";
final String value1 = "val1";
final String value2 = "2";
candidate.startElement(null, null, "plist", null);
candidate.startElement(null, null, "dict", null);
candidate.startElement(null, null, "key", null);
candidate.characters(attribute.toCharArray(), 0, attribute.length());
candidate.endElement(null, null, "key");
candidate.startElement(null, null, "array", null);
for (int i = 0; i < 3; i++) {
candidate.startElement(null, null, "dict", null);
candidate.startElement(null, null, "key", null);
candidate.characters(attr1.toCharArray(), 0, attr1.length());
candidate.endElement(null, null, "key");
candidate.startElement(null, null, "string", null);
candidate.characters(value1.toCharArray(), 0, value1.length());
candidate.endElement(null, null, "string");
candidate.startElement(null, null, "key", null);
candidate.characters(attr2.toCharArray(), 0, attr2.length());
candidate.endElement(null, null, "key");
candidate.startElement(null, null, "integer", null);
candidate.characters(value2.toCharArray(), 0, value2.length());
candidate.endElement(null, null, "integer");
candidate.startElement(null, null, "key", null);
candidate.characters(attr3.toCharArray(), 0, attr3.length());
candidate.endElement(null, null, "key");
candidate.startElement(null, null, "true", null);
candidate.endElement(null, null, "true");
candidate.endElement(null, null, "dict");
}
candidate.endElement(null, null, "array");
candidate.endElement(null, null, "dict");
candidate.endElement(null, null, "plist");
assertFalse(valueCollector.values.isEmpty());
assertTrue(valueCollector.values.size() == 9);
assertEquals(
"[ConfigurationValue [id=null, institutionId=1, configurationId=1, attributeId=1, listIndex=0, value=val1], "
+ "ConfigurationValue [id=null, institutionId=1, configurationId=1, attributeId=2, listIndex=0, value=2], "
+ "ConfigurationValue [id=null, institutionId=1, configurationId=1, attributeId=3, listIndex=0, value=true], "
+ "ConfigurationValue [id=null, institutionId=1, configurationId=1, attributeId=1, listIndex=1, value=val1], "
+ "ConfigurationValue [id=null, institutionId=1, configurationId=1, attributeId=2, listIndex=1, value=2], "
+ "ConfigurationValue [id=null, institutionId=1, configurationId=1, attributeId=3, listIndex=1, value=true], "
+ "ConfigurationValue [id=null, institutionId=1, configurationId=1, attributeId=1, listIndex=2, value=val1], "
+ "ConfigurationValue [id=null, institutionId=1, configurationId=1, attributeId=2, listIndex=2, value=2], "
+ "ConfigurationValue [id=null, institutionId=1, configurationId=1, attributeId=3, listIndex=2, value=true]]",
valueCollector.values.toString());
assertEquals(
"[attribute.attr1, attribute.attr2, attribute.attr3, "
+ "attribute.attr1, attribute.attr2, attribute.attr3, "
+ "attribute.attr1, attribute.attr2, attribute.attr3]",
attrNamesCollector.toString());
}
private static final class ValueCollector implements Consumer<ConfigurationValue> {
List<ConfigurationValue> values = new ArrayList<>();
@Override
public void accept(final ConfigurationValue value) {
this.values.add(value);
}
}
}

View file

@ -48,8 +48,7 @@ public class SebConfigEncryptionServiceImplTest {
sebConfigEncryptionServiceImpl.streamDecrypted( sebConfigEncryptionServiceImpl.streamDecrypted(
out2, out2,
new ByteArrayInputStream(plainWithHeader), new ByteArrayInputStream(plainWithHeader),
null, EncryptionContext.contextOf(Strategy.PASSWORD_PSWD, (CharSequence) null));
null);
out2.close(); out2.close();
@ -86,8 +85,7 @@ public class SebConfigEncryptionServiceImplTest {
sebConfigEncryptionServiceImpl.streamDecrypted( sebConfigEncryptionServiceImpl.streamDecrypted(
out2, out2,
new ByteArrayInputStream(byteArray), new ByteArrayInputStream(byteArray),
() -> pwd, EncryptionContext.contextOf(Strategy.PASSWORD_PSWD, pwd));
null);
final byte[] byteArray2 = out2.toByteArray(); final byte[] byteArray2 = out2.toByteArray();
assertNotNull(byteArray2); assertNotNull(byteArray2);