diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/Constants.java b/src/main/java/ch/ethz/seb/sebserver/gbl/Constants.java index 065c4903..7de5838d 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/Constants.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/Constants.java @@ -60,6 +60,15 @@ public final class Constants { public static final String XML_DICT_END = ""; + 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_REFRESH_TOKEN = "refresh_token"; public static final String OAUTH2_GRANT_TYPE_CLIENT_CREDENTIALS = "client_credentials"; diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java b/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java index 0af7a354..02197401 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java @@ -16,6 +16,7 @@ public final class API { ACTIVATE; } + public static final String SEB_FILE_EXTENSION = "seb"; public static final String PARAM_LOGO_IMAGE = "logoImageBase64"; public static final String PARAM_INSTITUTION_ID = "institutionId"; 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_ATTRIBUTE_ENDPOINT = "/configuration_attribute"; 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 VIEW_ENDPOINT = ORIENTATION_ENDPOINT + "/view"; diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/util/Utils.java b/src/main/java/ch/ethz/seb/sebserver/gbl/util/Utils.java index fc050adc..dc51c3ef 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/util/Utils.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/util/Utils.java @@ -46,6 +46,8 @@ public final class Utils { public static final Predicate TRUE_PREDICATE = v -> true; public static final Predicate FALSE_PREDICATE = v -> false; + public static final Runnable EMPTY_EXECUTION = () -> { + }; private static final Logger log = LoggerFactory.getLogger(Utils.class); diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/SebExamConfigPropForm.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/SebExamConfigPropForm.java index 827871c6..3ae72197 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/SebExamConfigPropForm.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/SebExamConfigPropForm.java @@ -8,12 +8,15 @@ package ch.ethz.seb.sebserver.gui.content; +import java.io.InputStream; import java.util.function.Function; +import java.util.function.Supplier; import org.eclipse.rap.rwt.RWT; import org.eclipse.rap.rwt.client.service.UrlLauncher; import org.eclipse.swt.SWT; import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Text; import org.slf4j.Logger; 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.user.UserInfo; 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.form.Form; import ch.ethz.seb.sebserver.gui.form.FormBuilder; import ch.ethz.seb.sebserver.gui.form.FormHandle; import ch.ethz.seb.sebserver.gui.service.ResourceService; 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.PageService; 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.auth.CurrentUser; 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.CustomVariant; @@ -58,6 +65,8 @@ public class SebExamConfigPropForm implements TemplateComposer { 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 = new LocTextKey("sebserver.examconfig.form.title.new"); private static final LocTextKey FORM_TITLE = @@ -68,7 +77,12 @@ public class SebExamConfigPropForm implements TemplateComposer { new LocTextKey("sebserver.examconfig.form.description"); private static final LocTextKey FORM_STATUS_TEXT_KEY = 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 = new LocTextKey("sebserver.examconfig.form.config-key.title"); @@ -201,6 +215,12 @@ public class SebExamConfigPropForm implements TemplateComposer { .noEventPropagation() .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) .withEntityKey(entityKey) .withExec(formHandle::processFormSave) @@ -247,4 +267,71 @@ public class SebExamConfigPropForm implements TemplateComposer { }; } + private static Function importConfigFunction(final PageService pageService) { + return action -> { + + final ModalInputDialog> dialog = + new ModalInputDialog>( + 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 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> { + + 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> compose(final Composite parent) { + + final FormHandle 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; + } + } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionDefinition.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionDefinition.java index df84aba0..0a5ea2a8 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionDefinition.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionDefinition.java @@ -393,6 +393,10 @@ public enum ActionDefinition { new LocTextKey("sebserver.examconfig.action.get-config-key"), ImageIcon.SECURE, ActionCategory.FORM), + SEB_EXAM_CONFIG_IMPORT_CONFIG( + new LocTextKey("sebserver.examconfig.action.import-config"), + ImageIcon.IMPORT, + ActionCategory.FORM), SEB_EXAM_CONFIG_MODIFY_FROM_LIST( new LocTextKey("sebserver.examconfig.action.list.modify"), diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/form/FileUploadFieldBuilder.java b/src/main/java/ch/ethz/seb/sebserver/gui/form/FileUploadFieldBuilder.java new file mode 100644 index 00000000..7cbb59b2 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/form/FileUploadFieldBuilder.java @@ -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 { + + private final Collection supportedFiles; + + FileUploadFieldBuilder( + final String name, + final LocTextKey label, + final String value, + final Collection 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); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/form/Form.java b/src/main/java/ch/ethz/seb/sebserver/gui/form/Form.java index 5937cf04..5641a600 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/form/Form.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/form/Form.java @@ -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.Utils; 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.Type; 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)); } - 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); imageUpload.setErrorHandler(createAccessor::setError); 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) { final FormFieldAccessor fieldAccessor = this.formFields.getFirst(attributeName); if (fieldAccessor == null) { @@ -152,6 +160,15 @@ public final class Form implements FormBinding { 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) { final FormFieldAccessor fieldAccessor = this.formFields.getFirst(attributeName); 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) { @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 /* diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/form/FormBuilder.java b/src/main/java/ch/ethz/seb/sebserver/gui/form/FormBuilder.java index ae7d2f8b..3ae7c926 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/form/FormBuilder.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/form/FormBuilder.java @@ -8,7 +8,9 @@ package ch.ethz.seb.sebserver.gui.form; +import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.function.BooleanSupplier; import java.util.function.Supplier; @@ -66,9 +68,9 @@ public class FormBuilder { this.pageContext = pageContext; this.form = new Form(pageService.getJSONMapper()); - this.formParent = this.widgetFactory - .formGrid(pageContext.getParent(), rows); - this.formParent.setData("TEST"); + this.formParent = this.widgetFactory.formGrid( + pageContext.getParent(), + rows); } public FormBuilder readonly(final boolean readonly) { @@ -252,6 +254,19 @@ public class FormBuilder { 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( final Composite parent, final LocTextKey locTextKey, diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/form/FormHandle.java b/src/main/java/ch/ethz/seb/sebserver/gui/form/FormHandle.java index 490c436e..c606754e 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/form/FormHandle.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/form/FormHandle.java @@ -54,6 +54,10 @@ public class FormHandle { this.i18nSupport = pageService.getI18nSupport(); } + public PageContext getContext() { + return this.pageContext; + } + public FormBinding getFormBinding() { return this.form; } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/form/ImageUploadFieldBuilder.java b/src/main/java/ch/ethz/seb/sebserver/gui/form/ImageUploadFieldBuilder.java index 2abb666b..2334326b 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/form/ImageUploadFieldBuilder.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/form/ImageUploadFieldBuilder.java @@ -14,7 +14,7 @@ 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.ImageUpload; +import ch.ethz.seb.sebserver.gui.widget.ImageUploadSelection; public final class ImageUploadFieldBuilder extends FieldBuilder { @@ -45,7 +45,7 @@ public final class ImageUploadFieldBuilder extends FieldBuilder { 1); final Composite fieldGrid = Form.createFieldGrid(builder.formParent, this.spanInput); - final ImageUpload imageUpload = builder.widgetFactory.imageUploadLocalized( + final ImageUploadSelection imageUpload = builder.widgetFactory.imageUploadLocalized( fieldGrid, new LocTextKey("sebserver.overall.upload"), builder.readonly || this.readonly, diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/i18n/PolyglotPageService.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/i18n/PolyglotPageService.java index be2d72ab..25bf2e53 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/i18n/PolyglotPageService.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/i18n/PolyglotPageService.java @@ -23,7 +23,7 @@ import org.eclipse.swt.widgets.Tree; import org.eclipse.swt.widgets.TreeItem; 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 { @@ -49,7 +49,7 @@ public interface PolyglotPageService { * @param locale the Locale to set */ void setPageLocale(Composite root, Locale locale); - void injectI18n(ImageUpload imageUpload, LocTextKey locTextKey); + void injectI18n(ImageUploadSelection imageUpload, LocTextKey locTextKey); void injectI18n(Label label, LocTextKey locTextKey); diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/i18n/impl/PolyglotPageServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/i18n/impl/PolyglotPageServiceImpl.java index b7c77074..f064d5eb 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/i18n/impl/PolyglotPageServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/i18n/impl/PolyglotPageServiceImpl.java @@ -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.page.ComposerService; 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 */ @Lazy @@ -72,8 +72,8 @@ public final class PolyglotPageServiceImpl implements PolyglotPageService { } @Override - public void injectI18n(final ImageUpload imageUpload, final LocTextKey locTextKey) { - final Consumer imageUploadFunction = iu -> { + public void injectI18n(final ImageUploadSelection imageUpload, final LocTextKey locTextKey) { + final Consumer imageUploadFunction = iu -> { if (locTextKey != null) { iu.setSelectionText(this.i18nSupport.getText(locTextKey)); } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/RestCall.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/RestCall.java index ac9c52ca..f247d080 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/RestCall.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/RestCall.java @@ -9,6 +9,7 @@ package ch.ethz.seb.sebserver.gui.service.remote.webservice.api; import java.io.IOException; +import java.io.InputStream; import java.util.Arrays; import java.util.HashMap; import java.util.List; @@ -185,6 +186,8 @@ public abstract class RestCall { private UriComponentsBuilder uriComponentsBuilder; private final HttpHeaders httpHeaders; private String body = null; + private InputStream streamingBody = null; + private final MultiValueMap queryParams; private final Map uriVariables; @@ -247,6 +250,11 @@ public abstract class RestCall { return this; } + if (body instanceof InputStream) { + this.streamingBody = (InputStream) body; + return this; + } + try { this.body = RestCall.this.jsonMapper.writeValueAsString(body); } catch (final JsonProcessingException e) { @@ -325,7 +333,9 @@ public abstract class RestCall { } 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); } else { return new HttpEntity<>(this.httpHeaders); diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/seb/examconfig/ImportExamConfig.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/seb/examconfig/ImportExamConfig.java new file mode 100644 index 00000000..317ae669 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/seb/examconfig/ImportExamConfig.java @@ -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 { + + public ImportExamConfig() { + super(new TypeKey<>( + CallType.UNDEFINED, + EntityType.CONFIGURATION, + new TypeReference() { + }), + HttpMethod.POST, + MediaType.APPLICATION_FORM_URLENCODED, + API.CONFIGURATION_NODE_ENDPOINT + + API.MODEL_ID_VAR_PATH_SEGMENT + + API.CONFIGURATION_IMPORT_PATH_SEGMENT); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/widget/FileUploadSelection.java b/src/main/java/ch/ethz/seb/sebserver/gui/widget/FileUploadSelection.java new file mode 100644 index 00000000..5ef0b2c5 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/widget/FileUploadSelection.java @@ -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 supportedFileExtensions = new ArrayList<>(); + + private final boolean readonly; + private final FileUpload fileUpload; + private final Label fileName; + + private Consumer 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 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; + } + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/widget/ImageUpload.java b/src/main/java/ch/ethz/seb/sebserver/gui/widget/ImageUploadSelection.java similarity index 66% rename from src/main/java/ch/ethz/seb/sebserver/gui/widget/ImageUpload.java rename to src/main/java/ch/ethz/seb/sebserver/gui/widget/ImageUploadSelection.java index fa6015df..96705d73 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/widget/ImageUpload.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/widget/ImageUploadSelection.java @@ -28,8 +28,6 @@ import org.eclipse.rap.fileupload.FileUploadReceiver; import org.eclipse.rap.rwt.RWT; import org.eclipse.rap.rwt.widgets.FileUpload; 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.ImageData; 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.ServerPushService; -public final class ImageUpload extends Composite { +public final class ImageUploadSelection extends Composite { 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 SUPPORTED_IMAGE_FILES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList( ".png", @@ -65,7 +63,7 @@ public final class ImageUpload extends Composite { private boolean loadNewImage = false; private boolean imageLoaded = false; - ImageUpload( + ImageUploadSelection( final Composite parent, final ServerPushService serverPushService, final I18nSupport i18nSupport, @@ -74,7 +72,12 @@ public final class ImageUpload extends Composite { final int maxHeight) { 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.maxWidth = maxWidth; @@ -85,55 +88,29 @@ public final class ImageUpload extends Composite { this.fileUpload.setImage(WidgetFactory.ImageIcon.IMPORT.getImage(parent.getDisplay())); this.fileUpload.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false)); - final FileUploadHandler uploadHandler = new FileUploadHandler(new 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")) { - 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(); + final FileUploadHandler uploadHandler = new FileUploadHandler(new ImageReceiver()); + this.fileUpload.addListener(SWT.Selection, event -> { + final String fileName = ImageUploadSelection.this.fileUpload.getFileName(); + if (fileName == null || !fileSupported(fileName)) { + if (ImageUploadSelection.this.errorHandler != null) { + final String text = i18nSupport.getText( + "sebserver.institution.form.logoImage.unsupportedFileType", + "Unsupported image file type selected"); + ImageUploadSelection.this.errorHandler.accept(text); } + + log.warn("Unsupported image file selected: {}", fileName); + + return; } - }); - - 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 (ImageUpload.this.errorHandler != null) { - final String text = i18nSupport.getText( - "sebserver.institution.form.logoImage.unsupportedFileType", - "Unsupported image file type selected"); - ImageUpload.this.errorHandler.accept(text); - } - - log.warn("Unsupported image file selected: {}", fileName); - - return; - } - ImageUpload.this.loadNewImage = true; - ImageUpload.this.imageLoaded = false; - ImageUpload.this.fileUpload.submit(uploadHandler.getUploadUrl()); - - ImageUpload.this.serverPushService.runServerPush( - new ServerPushContext(ImageUpload.this, ImageUpload::uploadInProgress), - 200, - ImageUpload::update); - } + ImageUploadSelection.this.loadNewImage = true; + ImageUploadSelection.this.imageLoaded = false; + ImageUploadSelection.this.fileUpload.submit(uploadHandler.getUploadUrl()); + ImageUploadSelection.this.serverPushService.runServerPush( + new ServerPushContext(ImageUploadSelection.this, ImageUploadSelection::uploadInProgress), + 200, + ImageUploadSelection::update); }); } else { this.fileUpload = null; @@ -142,7 +119,6 @@ public final class ImageUpload extends Composite { this.imageCanvas = new Composite(this, SWT.NONE); final GridData canvas = new GridData(SWT.FILL, SWT.FILL, true, true); this.imageCanvas.setLayoutData(canvas); - } public void setErrorHandler(final Consumer errorHandler) { @@ -172,12 +148,12 @@ public final class ImageUpload extends Composite { } 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; } private static final void update(final ServerPushContext context) { - final ImageUpload imageUpload = (ImageUpload) context.getAnchor(); + final ImageUploadSelection imageUpload = (ImageUploadSelection) context.getAnchor(); if (imageUpload.imageBase64 != null && imageUpload.loadNewImage && 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"); final Image image = new Image(imageUpload.imageCanvas.getDisplay(), input); @@ -218,4 +194,23 @@ public final class ImageUpload extends Composite { .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(); + } + } + } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/widget/WidgetFactory.java b/src/main/java/ch/ethz/seb/sebserver/gui/widget/WidgetFactory.java index beb3d8be..15e7de7a 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/widget/WidgetFactory.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/widget/WidgetFactory.java @@ -585,7 +585,7 @@ public class WidgetFactory { return thresholdList; } - public ImageUpload logoImageUploadLocalized( + public ImageUploadSelection logoImageUploadLocalized( final Composite parent, final LocTextKey locTextKey, final boolean readonly) { @@ -598,14 +598,14 @@ public class WidgetFactory { DefaultPageLayout.LOGO_IMAGE_MAX_HEIGHT); } - public ImageUpload imageUploadLocalized( + public ImageUploadSelection imageUploadLocalized( final Composite parent, final LocTextKey locTextKey, final boolean readonly, final int maxWidth, final int maxHeight) { - final ImageUpload imageUpload = new ImageUpload( + final ImageUploadSelection imageUpload = new ImageUploadSelection( parent, this.serverPushService, this.i18nSupport, @@ -617,4 +617,17 @@ public class WidgetFactory { return imageUpload; } + public FileUploadSelection fileUploadSelection( + final Composite parent, + final boolean readonly, + final Collection supportedFiles) { + + final FileUploadSelection fileUploadSelection = + new FileUploadSelection(parent, null, this.i18nSupport, readonly); + if (supportedFiles != null) { + supportedFiles.forEach(ext -> fileUploadSelection.withSupportFor(ext)); + } + return fileUploadSelection; + } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/SebConfigEncryptionService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/SebConfigEncryptionService.java index 2ab2ee1d..6d08dc44 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/SebConfigEncryptionService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/SebConfigEncryptionService.java @@ -10,9 +10,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig; import java.io.InputStream; import java.io.OutputStream; -import java.security.cert.Certificate; -import java.util.function.Function; -import java.util.function.Supplier; +import java.util.Arrays; import org.springframework.scheduling.annotation.Async; @@ -54,6 +52,15 @@ public interface SebConfigEncryptionService { 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. @@ -76,7 +83,6 @@ public interface SebConfigEncryptionService { void streamDecrypted( final OutputStream output, final InputStream input, - Supplier passwordSupplier, - Function certificateStore); + final SebConfigEncryptionContext context); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/SebExamConfigService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/SebExamConfigService.java index ef967ccc..579d120e 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/SebExamConfigService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/SebExamConfigService.java @@ -8,9 +8,11 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig; +import java.io.InputStream; import java.io.OutputStream; 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.ConfigurationValue; 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. */ Result 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 importFromXML(Long configNodeId, InputStream input, CharSequence password); + } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/ExamConfigIO.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/ExamConfigIO.java index b3bfe932..a7e16691 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/ExamConfigIO.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/ExamConfigIO.java @@ -19,12 +19,17 @@ import java.util.function.Predicate; import java.util.stream.Collectors; 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.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Lazy; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; +import org.xml.sax.SAXException; import ch.ethz.seb.sebserver.gbl.Constants; 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.ConfigurationDAO; 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.AttributeValueConverterService; 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 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 exportFormatBasedAttributeFilter(final ConfigurationFormat format) { // Filter originatorVersion according to: https://www.safeexambrowser.org/developer/seb-config-key.html 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 getConfigurationValueSupplier( final Long institutionId, final Long configurationId) { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/ExamConfigImportHandler.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/ExamConfigImportHandler.java new file mode 100644 index 00000000..41cf63f2 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/ExamConfigImportHandler.java @@ -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 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 valueConsumer; + private final Function attributeNameIdResolver; + private final Long institutionId; + private final Long configId; + + private final Stack stack = new Stack<>(); + + protected ExamConfigImportHandler( + final Long institutionId, + final Long configId, + final Consumer valueConsumer, + final Function 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(); + } + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/SebConfigEncryptionServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/SebConfigEncryptionServiceImpl.java index b7813160..b2940cab 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/SebConfigEncryptionServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/SebConfigEncryptionServiceImpl.java @@ -18,7 +18,6 @@ import java.util.Arrays; import java.util.Collection; import java.util.Map; import java.util.function.Function; -import java.util.function.Supplier; import java.util.stream.Collectors; import org.apache.commons.io.IOUtils; @@ -103,8 +102,7 @@ public final class SebConfigEncryptionServiceImpl implements SebConfigEncryption public void streamDecrypted( final OutputStream output, final InputStream input, - final Supplier passwordSupplier, - final Function certificateStore) { + final SebConfigEncryptionContext context) { PipedOutputStream pout = null; PipedInputStream pin = null; @@ -118,11 +116,6 @@ public final class SebConfigEncryptionServiceImpl implements SebConfigEncryption log.debug("Password decryption with strategy: {}", strategy); } - final EncryptionContext context = new EncryptionContext( - strategy, - (passwordSupplier != null) ? passwordSupplier.get() : null, - certificateStore); - getEncryptor(strategy) .getOrThrow() .decrypt(pout, input, context); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/SebExamConfigServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/SebExamConfigServiceImpl.java index f0a074c7..9c0b6bfe 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/SebExamConfigServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/SebExamConfigServiceImpl.java @@ -8,10 +8,14 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.impl; +import java.io.ByteArrayInputStream; import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; import java.io.PipedInputStream; import java.io.PipedOutputStream; +import java.io.SequenceInputStream; +import java.nio.ByteBuffer; import java.util.Collection; import java.util.List; 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.APIMessageException; 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.ConfigurationTableValues; 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.webservice.servicelayer.client.ClientCredentialService; 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.sebconfig.ConfigurationFormat; import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ConfigurationValueValidator; @@ -53,6 +59,7 @@ public class SebExamConfigServiceImpl implements SebExamConfigService { private final ExamConfigIO examConfigIO; private final ConfigurationAttributeDAO configurationAttributeDAO; + private final ConfigurationDAO configurationDAO; private final ExamConfigurationMapDAO examConfigurationMapDAO; private final Collection validators; private final ClientCredentialService clientCredentialService; @@ -62,6 +69,7 @@ public class SebExamConfigServiceImpl implements SebExamConfigService { protected SebExamConfigServiceImpl( final ExamConfigIO examConfigIO, final ConfigurationAttributeDAO configurationAttributeDAO, + final ConfigurationDAO configurationDAO, final ExamConfigurationMapDAO examConfigurationMapDAO, final Collection validators, final ClientCredentialService clientCredentialService, @@ -70,12 +78,12 @@ public class SebExamConfigServiceImpl implements SebExamConfigService { this.examConfigIO = examConfigIO; this.configurationAttributeDAO = configurationAttributeDAO; + this.configurationDAO = configurationDAO; this.examConfigurationMapDAO = examConfigurationMapDAO; this.validators = validators; this.clientCredentialService = clientCredentialService; this.zipService = zipService; this.sebConfigEncryptionService = sebConfigEncryptionService; - } @Override @@ -248,7 +256,7 @@ public class SebExamConfigServiceImpl implements SebExamConfigService { final Long configurationNodeId) { 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()) { @@ -288,7 +296,7 @@ public class SebExamConfigServiceImpl implements SebExamConfigService { return Result.of(configKey); } 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); } finally { try { @@ -307,11 +315,116 @@ public class SebExamConfigServiceImpl implements SebExamConfigService { } 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 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( final ConfigurationFormat exportFormat, final OutputStream out, @@ -319,7 +432,7 @@ public class SebExamConfigServiceImpl implements SebExamConfigService { final Long configurationNodeId) { 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; @@ -337,7 +450,7 @@ public class SebExamConfigServiceImpl implements SebExamConfigService { IOUtils.copyLarge(pin, out); } 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 { try { if (pin != null) { @@ -356,7 +469,7 @@ public class SebExamConfigServiceImpl implements SebExamConfigService { } if (log.isDebugEnabled()) { - log.debug("Finished to stream plain text SEB clonfiguration data"); + log.debug("Finished to stream plain text SEB Configuration export data"); } } } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ConfigurationNodeController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ConfigurationNodeController.java index 9eac0b79..dfa052e8 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ConfigurationNodeController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ConfigurationNodeController.java @@ -11,6 +11,7 @@ package ch.ethz.seb.sebserver.webservice.weblayer.api; import java.io.IOException; import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.mybatis.dynamic.sql.SqlTable; @@ -19,6 +20,7 @@ import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; 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.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; @@ -160,4 +162,24 @@ public class ConfigurationNodeController extends EntityController 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 attrNamesCollector = new ArrayList<>(); + final Function 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 attrNamesCollector = new ArrayList<>(); + final Function 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 { + List values = new ArrayList<>(); + + @Override + public void accept(final ConfigurationValue value) { + this.values.add(value); + } + } +} diff --git a/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/SebConfigEncryptionServiceImplTest.java b/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/SebConfigEncryptionServiceImplTest.java index 5f2d926f..41f33447 100644 --- a/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/SebConfigEncryptionServiceImplTest.java +++ b/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/SebConfigEncryptionServiceImplTest.java @@ -48,8 +48,7 @@ public class SebConfigEncryptionServiceImplTest { sebConfigEncryptionServiceImpl.streamDecrypted( out2, new ByteArrayInputStream(plainWithHeader), - null, - null); + EncryptionContext.contextOf(Strategy.PASSWORD_PSWD, (CharSequence) null)); out2.close(); @@ -86,8 +85,7 @@ public class SebConfigEncryptionServiceImplTest { sebConfigEncryptionServiceImpl.streamDecrypted( out2, new ByteArrayInputStream(byteArray), - () -> pwd, - null); + EncryptionContext.contextOf(Strategy.PASSWORD_PSWD, pwd)); final byte[] byteArray2 = out2.toByteArray(); assertNotNull(byteArray2);