From 7238369550b2d155fb5c53276e8d62f870f6fd3c Mon Sep 17 00:00:00 2001 From: anhefti Date: Wed, 19 Feb 2020 17:10:21 +0100 Subject: [PATCH] fallback attributes and new password attribute handling --- .../ch/ethz/seb/sebserver/gbl/Constants.java | 2 + .../seb/sebserver/gbl/api/POSTMapper.java | 365 ++-- .../gbl/model/sebconfig/SebClientConfig.java | 537 +++-- .../ethz/seb/sebserver/gbl/util/Cryptor.java | 101 + .../ch/ethz/seb/sebserver/gbl/util/Utils.java | 17 + .../gui/content/ExamToConfigBindingForm.java | 15 +- .../gui/content/SebClientConfigForm.java | 201 +- .../gui/content/SebExamConfigImportPopup.java | 562 ++--- .../gui/form/CheckboxFieldBuilder.java | 101 +- .../seb/sebserver/gui/form/FieldBuilder.java | 475 ++--- .../gui/form/FileUploadFieldBuilder.java | 105 +- .../ch/ethz/seb/sebserver/gui/form/Form.java | 1077 +++++----- .../seb/sebserver/gui/form/FormBuilder.java | 600 +++--- .../gui/form/ImageUploadFieldBuilder.java | 113 +- .../gui/form/PasswordFieldBuilder.java | 39 + .../gui/form/SelectionFieldBuilder.java | 298 +-- .../sebserver/gui/form/TextFieldBuilder.java | 5 +- .../gui/form/ThresholdListBuilder.java | 179 +- .../gui/service/ResourceService.java | 14 + ...Builder.java => PasswordFieldBuilder.java} | 359 ++-- .../service/page/impl/PageServiceImpl.java | 6 +- .../sebserver/gui/widget/PasswordInput.java | 132 ++ .../sebserver/gui/widget/WidgetFactory.java | 1532 +++++++------- .../client/ClientCredentialServiceImpl.java | 346 ++- .../webservice/servicelayer/dao/.gitignore | 1 - .../dao/ExamConfigurationMapDAO.java | 140 +- .../servicelayer/dao/SebClientConfigDAO.java | 126 +- .../dao/impl/ClientConnectionDAOImpl.java | 530 +++-- .../dao/impl/ClientEventDAOImpl.java | 522 +++-- .../impl/ConfigurationAttributeDAOImpl.java | 530 +++-- .../impl/ConfigurationDAOBatchService.java | 1680 ++++++++------- .../dao/impl/ExamConfigurationMapDAOImpl.java | 974 +++++---- .../dao/impl/SebClientConfigDAOImpl.java | 956 +++++---- .../sebconfig/ClientConfigService.java | 162 +- .../impl/ClientConfigServiceImpl.java | 691 +++--- .../sebconfig/impl/ExamConfigIO.java | 631 +++--- .../sebconfig/impl/ExamConfigServiceImpl.java | 864 ++++---- .../sebconfig/impl/ExamConfigXMLParser.java | 1090 +++++----- .../impl/converter/StringConverter.java | 233 ++- .../impl/init/XMLAttributeLoader.java | 166 +- .../api/ConfigurationNodeController.java | 1060 +++++----- .../api/SebClientConfigController.java | 381 ++-- src/main/resources/messages.properties | 38 +- src/main/resources/static/css/sebserver.css | 1862 +++++++++-------- .../resources/static/images/visibility.png | Bin 0 -> 255 bytes .../static/images/visibility_off.png | Bin 0 -> 290 bytes .../gui/integration/ClientConfigTest.java | 321 +-- .../integration/UseCasesIntegrationTest.java | 27 +- .../gui/integration/UsecaseTestUtils.java | 266 +-- .../client/ClientCredentialServiceTest.java | 113 +- .../impl/ExamConfigImportHandlerTest.java | 686 +++--- .../impl/converter/TableConverterTest.java | 658 +++--- 52 files changed, 11407 insertions(+), 10482 deletions(-) create mode 100644 src/main/java/ch/ethz/seb/sebserver/gbl/util/Cryptor.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/form/PasswordFieldBuilder.java rename src/main/java/ch/ethz/seb/sebserver/gui/service/examconfig/impl/{PassworFieldBuilder.java => PasswordFieldBuilder.java} (70%) create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/widget/PasswordInput.java delete mode 100644 src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/.gitignore create mode 100644 src/main/resources/static/images/visibility.png create mode 100644 src/main/resources/static/images/visibility_off.png 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 09d4551a..bb5d010e 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/Constants.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/Constants.java @@ -120,6 +120,8 @@ public final class Constants { public static final int GZIP_ID2 = 0x8B; public static final int GZIP_CM = 8; + public static final String SHA_256 = "SHA-256"; + public static final RGB WHITE_RGB = new RGB(255, 255, 255); public static final RGB BLACK_RGB = new RGB(0, 0, 0); diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/api/POSTMapper.java b/src/main/java/ch/ethz/seb/sebserver/gbl/api/POSTMapper.java index 0aae14bd..79279195 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/api/POSTMapper.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/api/POSTMapper.java @@ -1,178 +1,187 @@ -/* - * 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.gbl.api; - -import java.nio.CharBuffer; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Locale; -import java.util.Set; -import java.util.stream.Collectors; - -import org.apache.commons.lang3.BooleanUtils; -import org.apache.commons.lang3.StringUtils; -import org.joda.time.DateTime; -import org.joda.time.DateTimeZone; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import ch.ethz.seb.sebserver.gbl.Constants; -import ch.ethz.seb.sebserver.gbl.model.Domain; -import ch.ethz.seb.sebserver.gbl.model.exam.Indicator.Threshold; -import ch.ethz.seb.sebserver.gbl.util.Utils; - -public class POSTMapper { - - public static final POSTMapper EMPTY_MAP = new POSTMapper(null); - - protected final MultiValueMap params; - - public POSTMapper(final MultiValueMap params) { - super(); - this.params = params != null - ? new LinkedMultiValueMap<>(params) - : new LinkedMultiValueMap<>(); - } - - public String getString(final String name) { - return Utils.decodeFormURL_UTF_8(this.params.getFirst(name)); - } - - public char[] getCharArray(final String name) { - final String value = getString(name); - if (value == null || value.length() <= 0) { - return new char[] {}; - } - - return value.toCharArray(); - } - - public CharSequence getCharSequence(final String name) { - return CharBuffer.wrap(getCharArray(name)); - } - - public Long getLong(final String name) { - final String value = this.params.getFirst(name); - if (StringUtils.isBlank(value)) { - return null; - } - - return Long.parseLong(value); - } - - public Integer getInteger(final String name) { - final String value = this.params.getFirst(name); - if (value == null) { - return null; - } - - return Integer.parseInt(value); - } - - public Locale getLocale(final String name) { - final String value = this.params.getFirst(name); - if (value == null) { - return null; - } - - return Locale.forLanguageTag(value); - } - - public boolean getBoolean(final String name) { - return BooleanUtils.toBoolean(this.params.getFirst(name)); - } - - public Boolean getBooleanObject(final String name) { - return BooleanUtils.toBooleanObject(this.params.getFirst(name)); - } - - public Integer getBooleanAsInteger(final String name) { - final Boolean booleanObject = getBooleanObject(name); - if (booleanObject == null) { - return null; - } - return BooleanUtils.toIntegerObject(booleanObject); - } - - public DateTimeZone getDateTimeZone(final String name) { - final String value = this.params.getFirst(name); - if (value == null) { - return null; - } - try { - return DateTimeZone.forID(value); - } catch (final Exception e) { - return null; - } - } - - public Set getStringSet(final String name) { - final List list = this.params.get(name); - if (list == null) { - return Collections.emptySet(); - } - return Utils.immutableSetOf(list); - } - - public > T getEnum(final String name, final Class type, final T defaultValue) { - final T result = getEnum(name, type); - if (result == null) { - return defaultValue; - } - - return result; - } - - public > T getEnum(final String name, final Class type) { - final String value = this.params.getFirst(name); - if (value == null) { - return null; - } - try { - return Enum.valueOf(type, value); - } catch (final Exception e) { - return null; - } - } - - public DateTime getDateTime(final String name) { - final String value = this.params.getFirst(name); - if (value == null) { - return null; - } - - return Utils.toDateTime(value); - } - - public List getThresholds() { - final List thresholdStrings = this.params.get(Domain.THRESHOLD.REFERENCE_NAME); - if (thresholdStrings == null || thresholdStrings.isEmpty()) { - return Collections.emptyList(); - } - - return thresholdStrings.stream() - .map(ts -> { - try { - final String[] split = StringUtils.split(ts, Constants.EMBEDDED_LIST_SEPARATOR); - return new Threshold(Double.parseDouble(split[0]), split[1]); - } catch (final Exception e) { - return null; - } - }) - .collect(Collectors.toList()); - } - - @SuppressWarnings("unchecked") - public T putIfAbsent(final String name, final String value) { - this.params.putIfAbsent(name, Arrays.asList(value)); - return (T) this; - } - -} +/* + * 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.gbl.api; + +import java.nio.CharBuffer; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.StringUtils; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +import ch.ethz.seb.sebserver.gbl.Constants; +import ch.ethz.seb.sebserver.gbl.model.Domain; +import ch.ethz.seb.sebserver.gbl.model.exam.Indicator.Threshold; +import ch.ethz.seb.sebserver.gbl.util.Utils; + +public class POSTMapper { + + public static final POSTMapper EMPTY_MAP = new POSTMapper(null); + + protected final MultiValueMap params; + + public POSTMapper(final MultiValueMap params) { + super(); + this.params = params != null + ? new LinkedMultiValueMap<>(params) + : new LinkedMultiValueMap<>(); + } + + public String getString(final String name) { + return Utils.decodeFormURL_UTF_8(this.params.getFirst(name)); + } + + public char[] getCharArray(final String name) { + final String value = getString(name); + if (value == null || value.length() <= 0) { + return new char[] {}; + } + + return value.toCharArray(); + } + + public CharSequence getCharSequence(final String name) { + return CharBuffer.wrap(getCharArray(name)); + } + + public Long getLong(final String name) { + final String value = this.params.getFirst(name); + if (StringUtils.isBlank(value)) { + return null; + } + + return Long.parseLong(value); + } + + public Short getShort(final String name) { + final String value = this.params.getFirst(name); + if (StringUtils.isBlank(value)) { + return null; + } + + return Short.parseShort(value); + } + + public Integer getInteger(final String name) { + final String value = this.params.getFirst(name); + if (value == null) { + return null; + } + + return Integer.parseInt(value); + } + + public Locale getLocale(final String name) { + final String value = this.params.getFirst(name); + if (value == null) { + return null; + } + + return Locale.forLanguageTag(value); + } + + public boolean getBoolean(final String name) { + return BooleanUtils.toBoolean(this.params.getFirst(name)); + } + + public Boolean getBooleanObject(final String name) { + return BooleanUtils.toBooleanObject(this.params.getFirst(name)); + } + + public Integer getBooleanAsInteger(final String name) { + final Boolean booleanObject = getBooleanObject(name); + if (booleanObject == null) { + return null; + } + return BooleanUtils.toIntegerObject(booleanObject); + } + + public DateTimeZone getDateTimeZone(final String name) { + final String value = this.params.getFirst(name); + if (value == null) { + return null; + } + try { + return DateTimeZone.forID(value); + } catch (final Exception e) { + return null; + } + } + + public Set getStringSet(final String name) { + final List list = this.params.get(name); + if (list == null) { + return Collections.emptySet(); + } + return Utils.immutableSetOf(list); + } + + public > T getEnum(final String name, final Class type, final T defaultValue) { + final T result = getEnum(name, type); + if (result == null) { + return defaultValue; + } + + return result; + } + + public > T getEnum(final String name, final Class type) { + final String value = this.params.getFirst(name); + if (value == null) { + return null; + } + try { + return Enum.valueOf(type, value); + } catch (final Exception e) { + return null; + } + } + + public DateTime getDateTime(final String name) { + final String value = this.params.getFirst(name); + if (value == null) { + return null; + } + + return Utils.toDateTime(value); + } + + public List getThresholds() { + final List thresholdStrings = this.params.get(Domain.THRESHOLD.REFERENCE_NAME); + if (thresholdStrings == null || thresholdStrings.isEmpty()) { + return Collections.emptyList(); + } + + return thresholdStrings.stream() + .map(ts -> { + try { + final String[] split = StringUtils.split(ts, Constants.EMBEDDED_LIST_SEPARATOR); + return new Threshold(Double.parseDouble(split[0]), split[1]); + } catch (final Exception e) { + return null; + } + }) + .collect(Collectors.toList()); + } + + @SuppressWarnings("unchecked") + public T putIfAbsent(final String name, final String value) { + this.params.putIfAbsent(name, Arrays.asList(value)); + return (T) this; + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/sebconfig/SebClientConfig.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/sebconfig/SebClientConfig.java index f8739c8e..039de7a3 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/sebconfig/SebClientConfig.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/sebconfig/SebClientConfig.java @@ -1,200 +1,337 @@ -/* - * 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.gbl.model.sebconfig; - -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Size; - -import org.hibernate.validator.constraints.URL; -import org.joda.time.DateTime; -import org.joda.time.DateTimeZone; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonProperty; - -import ch.ethz.seb.sebserver.gbl.Constants; -import ch.ethz.seb.sebserver.gbl.api.EntityType; -import ch.ethz.seb.sebserver.gbl.api.POSTMapper; -import ch.ethz.seb.sebserver.gbl.model.Activatable; -import ch.ethz.seb.sebserver.gbl.model.Domain; -import ch.ethz.seb.sebserver.gbl.model.Domain.SEB_CLIENT_CONFIGURATION; -import ch.ethz.seb.sebserver.gbl.model.Entity; -import ch.ethz.seb.sebserver.gbl.model.GrantEntity; - -public final class SebClientConfig implements GrantEntity, Activatable { - - public static final String ATTR_FALLBACK_START_URL = "fallback_start_url"; - public static final String ATTR_CONFIRM_ENCRYPT_SECRET = "confirm_encrypt_secret"; - - public static final String FILTER_ATTR_CREATION_DATE = "creation_date"; - - @JsonProperty(SEB_CLIENT_CONFIGURATION.ATTR_ID) - public final Long id; - - @NotNull - @JsonProperty(SEB_CLIENT_CONFIGURATION.ATTR_INSTITUTION_ID) - public final Long institutionId; - - @NotNull(message = "clientconfig:name:notNull") - @Size(min = 3, max = 255, message = "clientconfig:name:size:{min}:{max}:${validatedValue}") - @JsonProperty(SEB_CLIENT_CONFIGURATION.ATTR_NAME) - public final String name; - - @JsonProperty(ATTR_FALLBACK_START_URL) - @URL(message = "clientconfig:fallback_start_url:invalidURL") - public final String fallbackStartURL; - - @JsonProperty(SEB_CLIENT_CONFIGURATION.ATTR_DATE) - public final DateTime date; - - @JsonProperty(SEB_CLIENT_CONFIGURATION.ATTR_ENCRYPT_SECRET) - public final CharSequence encryptSecret; - - @JsonProperty(ATTR_CONFIRM_ENCRYPT_SECRET) - public final CharSequence confirmEncryptSecret; - - @JsonProperty(SEB_CLIENT_CONFIGURATION.ATTR_ACTIVE) - public final Boolean active; - - @JsonCreator - public SebClientConfig( - @JsonProperty(SEB_CLIENT_CONFIGURATION.ATTR_ID) final Long id, - @JsonProperty(SEB_CLIENT_CONFIGURATION.ATTR_INSTITUTION_ID) final Long institutionId, - @JsonProperty(SEB_CLIENT_CONFIGURATION.ATTR_NAME) final String name, - @JsonProperty(ATTR_FALLBACK_START_URL) final String fallbackStartURL, - @JsonProperty(SEB_CLIENT_CONFIGURATION.ATTR_DATE) final DateTime date, - @JsonProperty(SEB_CLIENT_CONFIGURATION.ATTR_ENCRYPT_SECRET) final CharSequence encryptSecret, - @JsonProperty(ATTR_CONFIRM_ENCRYPT_SECRET) final CharSequence confirmEncryptSecret, - @JsonProperty(SEB_CLIENT_CONFIGURATION.ATTR_ACTIVE) final Boolean active) { - - this.id = id; - this.institutionId = institutionId; - this.name = name; - this.fallbackStartURL = fallbackStartURL; - this.date = date; - this.encryptSecret = encryptSecret; - this.confirmEncryptSecret = confirmEncryptSecret; - this.active = active; - } - - public SebClientConfig(final Long institutionId, final POSTMapper postParams) { - this.id = null; - this.institutionId = institutionId; - this.name = postParams.getString(Domain.SEB_CLIENT_CONFIGURATION.ATTR_NAME); - this.fallbackStartURL = postParams.getString(ATTR_FALLBACK_START_URL); - this.date = postParams.getDateTime(Domain.SEB_CLIENT_CONFIGURATION.ATTR_DATE); - this.encryptSecret = postParams.getCharSequence(Domain.SEB_CLIENT_CONFIGURATION.ATTR_ENCRYPT_SECRET); - this.confirmEncryptSecret = postParams.getCharSequence(ATTR_CONFIRM_ENCRYPT_SECRET); - this.active = false; - } - - @Override - public EntityType entityType() { - return EntityType.SEB_CLIENT_CONFIGURATION; - } - - @Override - public String getName() { - return this.name; - } - - public String getFallbackStartURL() { - return this.fallbackStartURL; - } - - @Override - public String getModelId() { - return (this.id != null) - ? String.valueOf(this.id) - : null; - } - - @Override - public boolean isActive() { - return this.active; - } - - @Override - public Long getInstitutionId() { - return this.institutionId; - } - - public Long getId() { - return this.id; - } - - public DateTime getDate() { - return this.date; - } - - @JsonIgnore - public CharSequence getEncryptSecret() { - return this.encryptSecret; - } - - @JsonIgnore - public CharSequence getConfirmEncryptSecret() { - return this.confirmEncryptSecret; - } - - @JsonIgnore - public boolean hasEncryptionSecret() { - return this.encryptSecret != null && this.encryptSecret.length() > 0; - } - - public Boolean getActive() { - return this.active; - } - - @Override - public Entity printSecureCopy() { - return new SebClientConfig( - this.id, - this.institutionId, - this.name, - this.fallbackStartURL, - this.date, - Constants.EMPTY_NOTE, - Constants.EMPTY_NOTE, - this.active); - } - - @Override - public String toString() { - final StringBuilder builder = new StringBuilder(); - builder.append("SebClientConfig [id="); - builder.append(this.id); - builder.append(", institutionId="); - builder.append(this.institutionId); - builder.append(", name="); - builder.append(this.name); - builder.append(", fallbackStartURL="); - builder.append(this.fallbackStartURL); - builder.append(", date="); - builder.append(this.date); - builder.append(", active="); - builder.append(this.active); - builder.append("]"); - return builder.toString(); - } - - public static final SebClientConfig createNew(final Long institutionId) { - return new SebClientConfig( - null, - institutionId, - null, - null, - DateTime.now(DateTimeZone.UTC), - null, - null, - false); - } - -} +/* + * 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.gbl.model.sebconfig; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +import org.hibernate.validator.constraints.URL; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; + +import ch.ethz.seb.sebserver.gbl.Constants; +import ch.ethz.seb.sebserver.gbl.api.EntityType; +import ch.ethz.seb.sebserver.gbl.api.POSTMapper; +import ch.ethz.seb.sebserver.gbl.model.Activatable; +import ch.ethz.seb.sebserver.gbl.model.Domain; +import ch.ethz.seb.sebserver.gbl.model.Domain.SEB_CLIENT_CONFIGURATION; +import ch.ethz.seb.sebserver.gbl.model.Entity; +import ch.ethz.seb.sebserver.gbl.model.GrantEntity; + +public final class SebClientConfig implements GrantEntity, Activatable { + + public static final String ATTR_CONFIG_PURPOSE = "sebConfigPurpose"; + public static final String ATTR_FALLBACK = "sebServerFallback "; + public static final String ATTR_FALLBACK_START_URL = "startURL"; + public static final String ATTR_FALLBACK_TIMEOUT = "sebServerFallbackTimeout"; + public static final String ATTR_FALLBACK_ATTEMPTS = "sebServerFallbackAttempts"; + public static final String ATTR_FALLBACK_ATTEMPT_INTERVAL = "sebServerFallbackAttemptInterval"; + public static final String ATTR_FALLBACK_PASSWORD = "sebServerFallbackPasswordHash"; + public static final String ATTR_FALLBACK_PASSWORD_CONFIRM = "sebServerFallbackPasswordHashConfirm"; + public static final String ATTR_QUIT_PASSWORD = "hashedQuitPassword"; + public static final String ATTR_QUIT_PASSWORD_CONFIRM = "hashedQuitPasswordConfirm"; + public static final String ATTR_ENCRYPT_SECRET_CONFIRM = "confirm_encrypt_secret"; + + public static final String FILTER_ATTR_CREATION_DATE = "creation_date"; + + public enum ConfigPurpose { + START_EXAM, + CONFIGURE_CLIENT + } + + @JsonProperty(SEB_CLIENT_CONFIGURATION.ATTR_ID) + public final Long id; + + @NotNull + @JsonProperty(SEB_CLIENT_CONFIGURATION.ATTR_INSTITUTION_ID) + public final Long institutionId; + + @NotNull(message = "clientconfig:name:notNull") + @Size(min = 3, max = 255, message = "clientconfig:name:size:{min}:{max}:${validatedValue}") + @JsonProperty(SEB_CLIENT_CONFIGURATION.ATTR_NAME) + public final String name; + + @NotNull(message = "clientconfig:sebConfigPurpose:notNull") + @JsonProperty(ATTR_CONFIG_PURPOSE) + public final ConfigPurpose configPurpose; + + @JsonProperty(ATTR_FALLBACK) + public final Boolean fallback; + + @JsonProperty(ATTR_FALLBACK_START_URL) + @URL(message = "clientconfig:startURL:invalidURL") + public final String fallbackStartURL; + + @JsonProperty(ATTR_FALLBACK_TIMEOUT) + public final Long fallbackTimeout; + + @JsonProperty(ATTR_FALLBACK_ATTEMPTS) + public final Short fallbackAttempts; + + @JsonProperty(ATTR_FALLBACK_ATTEMPT_INTERVAL) + public final Short fallbackAttemptInterval; + + @JsonProperty(ATTR_FALLBACK_PASSWORD) + public final CharSequence fallbackPassword; + + @JsonProperty(ATTR_FALLBACK_PASSWORD_CONFIRM) + public final CharSequence fallbackPasswordConfirm; + + @JsonProperty(ATTR_QUIT_PASSWORD) + public final CharSequence quitPassword; + + @JsonProperty(ATTR_QUIT_PASSWORD_CONFIRM) + public final CharSequence quitPasswordConfirm; + + @JsonProperty(SEB_CLIENT_CONFIGURATION.ATTR_DATE) + public final DateTime date; + + @JsonProperty(SEB_CLIENT_CONFIGURATION.ATTR_ENCRYPT_SECRET) + public final CharSequence encryptSecret; + + @JsonProperty(ATTR_ENCRYPT_SECRET_CONFIRM) + public final CharSequence encryptSecretConfirm; + + @JsonProperty(SEB_CLIENT_CONFIGURATION.ATTR_ACTIVE) + public final Boolean active; + + @JsonCreator + public SebClientConfig( + @JsonProperty(SEB_CLIENT_CONFIGURATION.ATTR_ID) final Long id, + @JsonProperty(SEB_CLIENT_CONFIGURATION.ATTR_INSTITUTION_ID) final Long institutionId, + @JsonProperty(SEB_CLIENT_CONFIGURATION.ATTR_NAME) final String name, + @JsonProperty(ATTR_CONFIG_PURPOSE) final ConfigPurpose configPurpose, + @JsonProperty(ATTR_FALLBACK) final Boolean fallback, + @JsonProperty(ATTR_FALLBACK_START_URL) final String fallbackStartURL, + @JsonProperty(ATTR_FALLBACK_TIMEOUT) final Long fallbackTimeout, + @JsonProperty(ATTR_FALLBACK_ATTEMPTS) final Short fallbackAttempts, + @JsonProperty(ATTR_FALLBACK_ATTEMPT_INTERVAL) final Short fallbackAttemptInterval, + @JsonProperty(ATTR_FALLBACK_PASSWORD) final CharSequence fallbackPassword, + @JsonProperty(ATTR_FALLBACK_PASSWORD_CONFIRM) final CharSequence fallbackPasswordConfirm, + @JsonProperty(ATTR_QUIT_PASSWORD) final CharSequence quitPassword, + @JsonProperty(ATTR_QUIT_PASSWORD_CONFIRM) final CharSequence quitPasswordConfirm, + @JsonProperty(SEB_CLIENT_CONFIGURATION.ATTR_DATE) final DateTime date, + @JsonProperty(SEB_CLIENT_CONFIGURATION.ATTR_ENCRYPT_SECRET) final CharSequence encryptSecret, + @JsonProperty(ATTR_ENCRYPT_SECRET_CONFIRM) final CharSequence encryptSecretConfirm, + @JsonProperty(SEB_CLIENT_CONFIGURATION.ATTR_ACTIVE) final Boolean active) { + + this.id = id; + this.institutionId = institutionId; + this.name = name; + this.configPurpose = configPurpose; + this.fallback = fallback; + this.fallbackStartURL = fallbackStartURL; + this.fallbackTimeout = fallbackTimeout; + this.fallbackAttempts = fallbackAttempts; + this.fallbackAttemptInterval = fallbackAttemptInterval; + this.fallbackPassword = fallbackPassword; + this.fallbackPasswordConfirm = fallbackPasswordConfirm; + this.quitPassword = quitPassword; + this.quitPasswordConfirm = quitPasswordConfirm; + this.date = date; + this.encryptSecret = encryptSecret; + this.encryptSecretConfirm = encryptSecretConfirm; + this.active = active; + } + + public SebClientConfig(final Long institutionId, final POSTMapper postParams) { + this.id = null; + this.institutionId = institutionId; + this.name = postParams.getString(Domain.SEB_CLIENT_CONFIGURATION.ATTR_NAME); + this.configPurpose = postParams.getEnum(ATTR_CONFIG_PURPOSE, ConfigPurpose.class); + this.fallback = postParams.getBoolean(ATTR_FALLBACK); + this.fallbackStartURL = postParams.getString(ATTR_FALLBACK_START_URL); + this.fallbackTimeout = postParams.getLong(ATTR_FALLBACK_TIMEOUT); + this.fallbackAttempts = postParams.getShort(ATTR_FALLBACK_ATTEMPTS); + this.fallbackAttemptInterval = postParams.getShort(ATTR_FALLBACK_ATTEMPT_INTERVAL); + this.fallbackPassword = postParams.getCharSequence(ATTR_FALLBACK_PASSWORD); + this.fallbackPasswordConfirm = postParams.getCharSequence(ATTR_FALLBACK_PASSWORD_CONFIRM); + this.quitPassword = postParams.getCharSequence(ATTR_QUIT_PASSWORD); + this.quitPasswordConfirm = postParams.getCharSequence(ATTR_QUIT_PASSWORD_CONFIRM); + this.date = postParams.getDateTime(Domain.SEB_CLIENT_CONFIGURATION.ATTR_DATE); + this.encryptSecret = postParams.getCharSequence(Domain.SEB_CLIENT_CONFIGURATION.ATTR_ENCRYPT_SECRET); + this.encryptSecretConfirm = postParams.getCharSequence(ATTR_ENCRYPT_SECRET_CONFIRM); + this.active = false; + } + + @Override + public EntityType entityType() { + return EntityType.SEB_CLIENT_CONFIGURATION; + } + + @Override + public String getName() { + return this.name; + } + + public String getFallbackStartURL() { + return this.fallbackStartURL; + } + + @Override + public String getModelId() { + return (this.id != null) + ? String.valueOf(this.id) + : null; + } + + @Override + public boolean isActive() { + return this.active; + } + + @Override + public Long getInstitutionId() { + return this.institutionId; + } + + public Long getId() { + return this.id; + } + + public ConfigPurpose getConfigPurpose() { + return configPurpose; + } + + public Boolean getFallback() { + return fallback; + } + + public Long getFallbackTimeout() { + return fallbackTimeout; + } + + public Short getFallbackAttempts() { + return fallbackAttempts; + } + + public Short getFallbackAttemptInterval() { + return fallbackAttemptInterval; + } + + public CharSequence getFallbackPassword() { + return fallbackPassword; + } + + @JsonIgnore + public CharSequence getFallbackPasswordConfirm() { + return fallbackPasswordConfirm; + } + + public CharSequence getQuitPassword() { + return quitPassword; + } + + @JsonIgnore + public CharSequence getQuitPasswordConfirm() { + return quitPasswordConfirm; + } + + public DateTime getDate() { + return this.date; + } + + public CharSequence getEncryptSecret() { + return this.encryptSecret; + } + + @JsonIgnore + public CharSequence getEncryptSecretConfirm() { + return this.encryptSecretConfirm; + } + + @JsonIgnore + public boolean hasEncryptionSecret() { + return this.encryptSecret != null && this.encryptSecret.length() > 0; + } + + @JsonIgnore + public boolean hasFallbackPassword() { + return this.fallbackPassword != null && this.fallbackPassword.length() > 0; + } + + @JsonIgnore + public boolean hasQuitPassword() { + return this.quitPassword != null && this.quitPassword.length() > 0; + } + + public Boolean getActive() { + return this.active; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("SebClientConfig{"); + sb.append("id=").append(id); + sb.append(", institutionId=").append(institutionId); + sb.append(", name='").append(name).append('\''); + sb.append(", configPurpose=").append(configPurpose); + sb.append(", fallback=").append(fallback); + sb.append(", fallbackStartURL='").append(fallbackStartURL).append('\''); + sb.append(", fallbackTimeout=").append(fallbackTimeout); + sb.append(", fallbackAttempts=").append(fallbackAttempts); + sb.append(", fallbackAttemptInterval=").append(fallbackAttemptInterval); + sb.append(", fallbackPassword=").append(fallbackPassword); + sb.append(", fallbackPasswordConfirm=").append(fallbackPasswordConfirm); + sb.append(", date=").append(date); + sb.append(", encryptSecret=").append(encryptSecret); + sb.append(", encryptSecretConfirm=").append(encryptSecretConfirm); + sb.append(", active=").append(active); + sb.append('}'); + return sb.toString(); + } + + @Override + public Entity printSecureCopy() { + return new SebClientConfig( + this.id, + this.institutionId, + this.name, + this.configPurpose, + this.fallback, + this.fallbackStartURL, + this.fallbackTimeout, + this.fallbackAttempts, + this.fallbackAttemptInterval, + Constants.EMPTY_NOTE, + Constants.EMPTY_NOTE, + Constants.EMPTY_NOTE, + Constants.EMPTY_NOTE, + this.date, + Constants.EMPTY_NOTE, + Constants.EMPTY_NOTE, + this.active); + } + + public static SebClientConfig createNew(final Long institutionId) { + return new SebClientConfig( + null, + institutionId, + null, + ConfigPurpose.START_EXAM, + false, + null, + null, + null, + null, + null, + null, + null, + null, + DateTime.now(DateTimeZone.UTC), + null, + null, + false); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/util/Cryptor.java b/src/main/java/ch/ethz/seb/sebserver/gbl/util/Cryptor.java new file mode 100644 index 00000000..f86c579c --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/util/Cryptor.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2020 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.gbl.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Lazy; +import org.springframework.core.env.Environment; +import org.springframework.security.crypto.encrypt.Encryptors; +import org.springframework.security.crypto.keygen.KeyGenerators; +import org.springframework.stereotype.Service; + +@Lazy +@Service +public class Cryptor { + + private static final Logger log = LoggerFactory.getLogger(Cryptor.class); + + public static final String SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY = "sebserver.webservice.internalSecret"; + + private final Environment environment; + + public Cryptor(final Environment environment) { + this.environment = environment; + } + + public CharSequence encrypt(final CharSequence text) { + + final CharSequence secret = this.environment + .getProperty(SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY); + + return encrypt(text, secret); + } + + public CharSequence decrypt(final CharSequence text) { + + final CharSequence secret = this.environment + .getProperty(SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY); + + return decrypt(text, secret); + } + + public static CharSequence encrypt(final CharSequence text, final CharSequence secret) { + if (text == null) { + throw new IllegalArgumentException("Text has null reference"); + } + + if (secret == null) { + log.warn("No internal secret supplied: skip encryption"); + return text; + } + + try { + + final CharSequence salt = KeyGenerators.string().generateKey(); + final CharSequence cipher = Encryptors + .delux(secret, salt) + .encrypt(text.toString()); + + return new StringBuilder(cipher) + .append(salt); + + } catch (final Exception e) { + log.error("Failed to encrypt text: ", e); + throw e; + } + } + + public static CharSequence decrypt(final CharSequence cipher, final CharSequence secret) { + if (cipher == null) { + throw new IllegalArgumentException("Cipher has null reference"); + } + + if (secret == null) { + log.warn("No internal secret supplied: skip decryption"); + return cipher; + } + + try { + + final int length = cipher.length(); + final int cipherTextLength = length - 16; + final CharSequence salt = cipher.subSequence(cipherTextLength, length); + final CharSequence cipherText = cipher.subSequence(0, cipherTextLength); + + return Encryptors + .delux(secret, salt) + .decrypt(cipherText.toString()); + + } catch (final Exception e) { + log.error("Failed to decrypt text: ", e); + throw e; + } + } +} 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 7cb29e6a..385c4d45 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 @@ -14,6 +14,8 @@ import java.net.URLEncoder; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -28,6 +30,7 @@ import java.util.function.Predicate; import java.util.stream.Collector; import java.util.stream.Collectors; +import org.apache.commons.codec.binary.Hex; import org.apache.commons.lang3.StringUtils; import org.apache.commons.text.StringEscapeUtils; import org.eclipse.swt.graphics.RGB; @@ -478,6 +481,20 @@ public final class Utils { return (text == null) ? null : Constants.PERCENTAGE + text + Constants.PERCENTAGE; } + public static String hash_SHA_256_Base_16(final CharSequence chars) { + if (chars == null) { + return null; + } + + try { + final MessageDigest digest = MessageDigest.getInstance(Constants.SHA_256); + final byte[] encodedHash = digest.digest(toByteArray(chars)); + return Hex.encodeHexString(encodedHash); + } catch (NoSuchAlgorithmException nsae) { + throw new RuntimeException("Failed to hash text: ", nsae); + } + } + @SuppressWarnings("unchecked") public static Predicate truePredicate() { return (Predicate) TRUE_PREDICATE; diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamToConfigBindingForm.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamToConfigBindingForm.java index 8f46edb4..98cf8309 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamToConfigBindingForm.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamToConfigBindingForm.java @@ -213,7 +213,7 @@ final class ExamToConfigBindingForm { String.valueOf(examConfigurationMap.configurationNodeId), resourceService::examConfigurationSelectionResources) .withSelectionListener(form -> updateFormValuesFromConfigSelection(form, resourceService)) - .mandatory()) + .mandatory()) .addField(FormBuilder.text( Domain.CONFIGURATION_NODE.ATTR_DESCRIPTION, @@ -228,14 +228,15 @@ final class ExamToConfigBindingForm { resourceService.localizedExamConfigStatusName(examConfigurationMap)) .readonly(true)) - .addField(FormBuilder.text( + .addField(FormBuilder.password( Domain.EXAM_CONFIGURATION_MAP.ATTR_ENCRYPT_SECRET, - FORM_ENCRYPT_SECRET_TEXT_KEY) - .asPasswordField()) - .addField(FormBuilder.text( + FORM_ENCRYPT_SECRET_TEXT_KEY, + examConfigurationMap.encryptSecret)) + + .addField(FormBuilder.password( ExamConfigurationMap.ATTR_CONFIRM_ENCRYPT_SECRET, - FORM_CONFIRM_ENCRYPT_SECRET_TEXT_KEY) - .asPasswordField()) + FORM_CONFIRM_ENCRYPT_SECRET_TEXT_KEY, + examConfigurationMap.encryptSecret)) .build(); diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/SebClientConfigForm.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/SebClientConfigForm.java index 16704734..17b355ca 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/SebClientConfigForm.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/SebClientConfigForm.java @@ -8,8 +8,14 @@ package ch.ethz.seb.sebserver.gui.content; +import ch.ethz.seb.sebserver.gbl.Constants; +import ch.ethz.seb.sebserver.gui.service.page.PageMessageException; +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.StringUtils; import org.eclipse.rap.rwt.RWT; import org.eclipse.rap.rwt.client.service.UrlLauncher; +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; @@ -42,6 +48,10 @@ 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.WidgetFactory; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + @Lazy @Component @GuiProfile @@ -53,15 +63,51 @@ public class SebClientConfigForm implements TemplateComposer { new LocTextKey("sebserver.clientconfig.form.title"); private static final LocTextKey FORM_NAME_TEXT_KEY = new LocTextKey("sebserver.clientconfig.form.name"); - private static final LocTextKey FORM_FALLBACK_URL_TEXT_KEY = - new LocTextKey("sebserver.clientconfig.form.fallback-url"); + private static final LocTextKey FORM_DATE_TEXT_KEY = new LocTextKey("sebserver.clientconfig.form.date"); + + private static final LocTextKey CLIENT_PURPOSE_TEXT_KEY = + new LocTextKey("sebserver.clientconfig.form.sebConfigPurpose"); + private static final LocTextKey FALLBACK_TEXT_KEY = + new LocTextKey("sebserver.clientconfig.form.fallback"); + private static final LocTextKey FALLBACK_URL_TEXT_KEY = + new LocTextKey("sebserver.clientconfig.form.fallback-url"); + private static final LocTextKey FALLBACK_TIMEOUT_TEXT_KEY = + new LocTextKey("sebserver.clientconfig.form.sebServerFallbackTimeout"); + private static final LocTextKey FALLBACK_ATTEMPTS_TEXT_KEY = + new LocTextKey("sebserver.clientconfig.form.sebServerFallbackAttempts"); + private static final LocTextKey FALLBACK_ATTEMPT_INTERVAL_TEXT_KEY = + new LocTextKey("sebserver.clientconfig.form.sebServerFallbackAttemptInterval"); + private static final LocTextKey FALLBACK_PASSWORD_TEXT_KEY = + new LocTextKey("sebserver.clientconfig.form.sebServerFallbackPasswordHash"); + private static final LocTextKey FALLBACK_PASSWORD_CONFIRM_TEXT_KEY = + new LocTextKey("sebserver.clientconfig.form.sebServerFallbackPasswordHash"); + private static final LocTextKey QUIT_PASSWORD_TEXT_KEY = + new LocTextKey("sebserver.clientconfig.form.hashedQuitPassword"); + private static final LocTextKey QUIT_PASSWORD_CONFIRM_TEXT_KEY = + new LocTextKey("sebserver.clientconfig.form.hashedQuitPassword.confirm"); + private static final LocTextKey FORM_ENCRYPT_SECRET_TEXT_KEY = new LocTextKey("sebserver.clientconfig.form.encryptSecret"); private static final LocTextKey FORM_CONFIRM_ENCRYPT_SECRET_TEXT_KEY = new LocTextKey("sebserver.clientconfig.form.encryptSecret.confirm"); + private static final Set FALLBACK_ATTRIBUTES = new HashSet<>(Arrays.asList( + SebClientConfig.ATTR_FALLBACK_START_URL, + SebClientConfig.ATTR_FALLBACK_ATTEMPT_INTERVAL, + SebClientConfig.ATTR_FALLBACK_ATTEMPTS, + SebClientConfig.ATTR_FALLBACK_TIMEOUT, + SebClientConfig.ATTR_FALLBACK_PASSWORD, + SebClientConfig.ATTR_FALLBACK_PASSWORD_CONFIRM, + SebClientConfig.ATTR_QUIT_PASSWORD, + SebClientConfig.ATTR_QUIT_PASSWORD_CONFIRM + )); + + private static final String FALLBACK_DEFAULT_TIME = String.valueOf(2 * Constants.MINUTE_IN_MILLIS); + private static final String FALLBACK_DEFAULT_ATTEMPTS = String.valueOf(5); + private static final String FALLBACK_DEFAULT_ATTEMPT_INTERVAL = String.valueOf(2 * Constants.SECOND_IN_MILLIS); + private final PageService pageService; private final RestService restService; private final CurrentUser currentUser; @@ -131,32 +177,147 @@ public class SebClientConfigForm implements TemplateComposer { .putStaticValue( Domain.SEB_CLIENT_CONFIGURATION.ATTR_INSTITUTION_ID, String.valueOf(clientConfig.getInstitutionId())) - .addField(FormBuilder.text( - Domain.SEB_CLIENT_CONFIGURATION.ATTR_NAME, - FORM_NAME_TEXT_KEY, - clientConfig.name)) - .addField(FormBuilder.text( - SebClientConfig.ATTR_FALLBACK_START_URL, - FORM_FALLBACK_URL_TEXT_KEY, - clientConfig.fallbackStartURL)) + .addFieldIf(() -> !isNew, () -> FormBuilder.text( Domain.SEB_CLIENT_CONFIGURATION.ATTR_DATE, FORM_DATE_TEXT_KEY, i18nSupport.formatDisplayDateWithTimeZone(clientConfig.date)) .readonly(true)) + .addField(FormBuilder.text( + Domain.SEB_CLIENT_CONFIGURATION.ATTR_NAME, + FORM_NAME_TEXT_KEY, + clientConfig.name) + .mandatory(!isReadonly)) + + .addField(FormBuilder.singleSelection( + SebClientConfig.ATTR_CONFIG_PURPOSE, + CLIENT_PURPOSE_TEXT_KEY, + clientConfig.configPurpose != null + ? clientConfig.configPurpose.name() + : SebClientConfig.ConfigPurpose.START_EXAM.name(), + () -> pageService.getResourceService().sebClientConfigPurposeResources()) + .mandatory(!isReadonly)) + + .withDefaultSpanInput(3) + .addField(FormBuilder.password( Domain.SEB_CLIENT_CONFIGURATION.ATTR_ENCRYPT_SECRET, - FORM_ENCRYPT_SECRET_TEXT_KEY) - .asPasswordField()) + FORM_ENCRYPT_SECRET_TEXT_KEY, + clientConfig.getEncryptSecret())) + + .withDefaultSpanEmptyCell(3) + .addFieldIf( + () -> !isReadonly, + () -> FormBuilder.password( + SebClientConfig.ATTR_ENCRYPT_SECRET_CONFIRM, + FORM_CONFIRM_ENCRYPT_SECRET_TEXT_KEY, + clientConfig.getEncryptSecret())) + + .addField(FormBuilder.checkbox( + SebClientConfig.ATTR_FALLBACK, + FALLBACK_TEXT_KEY, + clientConfig.fallback != null + ? clientConfig.fallback.toString() + : Constants.FALSE_STRING) + ) + + .withDefaultSpanInput(5) .addField(FormBuilder.text( - SebClientConfig.ATTR_CONFIRM_ENCRYPT_SECRET, - FORM_CONFIRM_ENCRYPT_SECRET_TEXT_KEY) - .asPasswordField()) + SebClientConfig.ATTR_FALLBACK_START_URL, + FALLBACK_URL_TEXT_KEY, + clientConfig.fallbackStartURL) + .mandatory(!isReadonly)) + + .withDefaultSpanEmptyCell(1) + .withDefaultSpanInput(2) + .addField(FormBuilder.text( + SebClientConfig.ATTR_FALLBACK_ATTEMPTS, + FALLBACK_ATTEMPTS_TEXT_KEY, + clientConfig.fallbackAttempts != null + ? String.valueOf(clientConfig.fallbackAttempts) + : FALLBACK_DEFAULT_ATTEMPTS) + .asNumber(this::checkNaturalNumber) + .mandatory(!isReadonly)) + + .withDefaultSpanEmptyCell(0) + .withEmptyCellSeparation(false) + .withDefaultSpanLabel(1) + .addField(FormBuilder.text( + SebClientConfig.ATTR_FALLBACK_ATTEMPT_INTERVAL, + FALLBACK_ATTEMPT_INTERVAL_TEXT_KEY, + clientConfig.fallbackAttemptInterval != null + ? String.valueOf(clientConfig.fallbackAttemptInterval) + : FALLBACK_DEFAULT_ATTEMPT_INTERVAL) + .asNumber(this::checkNaturalNumber) + .mandatory(!isReadonly)) + + .withEmptyCellSeparation(true) + .withDefaultSpanEmptyCell(1) + .withDefaultSpanLabel(2) + .addField(FormBuilder.text( + SebClientConfig.ATTR_FALLBACK_TIMEOUT, + FALLBACK_TIMEOUT_TEXT_KEY, + clientConfig.fallbackTimeout != null + ? String.valueOf(clientConfig.fallbackTimeout) + : FALLBACK_DEFAULT_TIME) + .asNumber(this::checkNaturalNumber) + .mandatory(!isReadonly)) + + .withEmptyCellSeparation(true) + .withDefaultSpanEmptyCell(4) + .withDefaultSpanInput(2) + .withDefaultSpanLabel(2) + .addField(FormBuilder.password( + SebClientConfig.ATTR_FALLBACK_PASSWORD, + FALLBACK_PASSWORD_TEXT_KEY, + clientConfig.getFallbackPassword())) + + .withEmptyCellSeparation(false) + .withDefaultSpanLabel(1) + .addField(FormBuilder.password( + SebClientConfig.ATTR_QUIT_PASSWORD, + QUIT_PASSWORD_TEXT_KEY, + clientConfig.getQuitPassword())) + + .withEmptyCellSeparation(true) + .withDefaultSpanEmptyCell(1) + .withDefaultSpanInput(2) + .withDefaultSpanLabel(2) + .addFieldIf( + () -> !isReadonly, + () -> FormBuilder.password( + SebClientConfig.ATTR_FALLBACK_PASSWORD_CONFIRM, + FALLBACK_PASSWORD_CONFIRM_TEXT_KEY, + clientConfig.getFallbackPasswordConfirm())) + + .withEmptyCellSeparation(false) + .withDefaultSpanLabel(1) + .addFieldIf( + () -> !isReadonly, + () -> FormBuilder.password( + SebClientConfig.ATTR_QUIT_PASSWORD_CONFIRM, + QUIT_PASSWORD_CONFIRM_TEXT_KEY, + clientConfig.getQuitPasswordConfirm())) + + .buildFor((isNew) ? this.restService.getRestCall(NewClientConfig.class) : this.restService.getRestCall(SaveClientConfig.class)); + formHandle.process( + FALLBACK_ATTRIBUTES::contains, + ffa -> ffa.setVisible(BooleanUtils.isTrue(clientConfig.fallback)) + ); + + formHandle.getForm().getFieldInput(SebClientConfig.ATTR_FALLBACK) + .addListener(SWT.Selection, event -> { + formHandle.process( + FALLBACK_ATTRIBUTES::contains, + ffa -> ffa.setVisible(((Button) event.widget).getSelection()) + ); + }); + final UrlLauncher urlLauncher = RWT.getClient().getService(UrlLauncher.class); this.pageService.pageActionBuilder(formContext.clearEntityKeys()) @@ -208,4 +369,14 @@ public class SebClientConfigForm implements TemplateComposer { .publishIf(() -> !isReadonly); } + private void checkNaturalNumber(String value) { + if (StringUtils.isBlank(value)) { + return; + } + long num = Long.parseLong(value); + if (num < 0) { + throw new PageMessageException("Number must be positive"); + } + } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/SebExamConfigImportPopup.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/SebExamConfigImportPopup.java index 261d14f4..c2efff12 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/SebExamConfigImportPopup.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/SebExamConfigImportPopup.java @@ -1,281 +1,281 @@ -/* - * 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.content; - -import java.io.InputStream; -import java.util.List; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.function.Supplier; - -import org.apache.commons.lang3.StringUtils; -import org.eclipse.swt.widgets.Composite; -import org.eclipse.swt.widgets.Control; - -import ch.ethz.seb.sebserver.gbl.api.API; -import ch.ethz.seb.sebserver.gbl.api.APIMessage; -import ch.ethz.seb.sebserver.gbl.api.EntityType; -import ch.ethz.seb.sebserver.gbl.model.Domain; -import ch.ethz.seb.sebserver.gbl.model.EntityKey; -import ch.ethz.seb.sebserver.gbl.model.sebconfig.Configuration; -import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode; -import ch.ethz.seb.sebserver.gbl.util.Result; -import ch.ethz.seb.sebserver.gbl.util.Tuple; -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.PageMessageException; -import ch.ethz.seb.sebserver.gui.service.page.PageService; -import ch.ethz.seb.sebserver.gui.service.page.event.ActionEvent; -import ch.ethz.seb.sebserver.gui.service.page.impl.ModalInputDialog; -import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction; -import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall; -import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCallError; -import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.examconfig.ImportExamConfigOnExistingConfig; -import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.examconfig.ImportNewExamConfig; -import ch.ethz.seb.sebserver.gui.widget.FileUploadSelection; - -final class SebExamConfigImportPopup { - - private final static PageMessageException MISSING_PASSWORD = new PageMessageException( - new LocTextKey("sebserver.examconfig.action.import.missing-password")); - - static Function importFunction( - final PageService pageService, - final boolean newConfig) { - - return action -> { - - final ModalInputDialog> dialog = - new ModalInputDialog>( - action.pageContext().getParent().getShell(), - pageService.getWidgetFactory()) - .setLargeDialogWidth(); - - final ImportFormContext importFormContext = new ImportFormContext( - pageService, - action.pageContext(), - newConfig); - - dialog.open( - SebExamConfigPropForm.FORM_IMPORT_TEXT_KEY, - (Predicate>) formHandle -> doImport( - pageService, - formHandle, - newConfig), - importFormContext::cancelUpload, - importFormContext); - - return action; - }; - } - - private static final boolean doImport( - final PageService pageService, - final FormHandle formHandle, - final boolean newConfig) { - - try { - final Form form = formHandle.getForm(); - final EntityKey entityKey = formHandle.getContext().getEntityKey(); - final Control fieldControl = form.getFieldControl(API.IMPORT_FILE_ATTR_NAME); - final PageContext context = formHandle.getContext(); - - // Ad-hoc field validation - formHandle.process(name -> true, field -> field.resetError()); - final String fieldValue = form.getFieldValue(Domain.CONFIGURATION_NODE.ATTR_NAME); - if (StringUtils.isBlank(fieldValue)) { - form.setFieldError( - Domain.CONFIGURATION_NODE.ATTR_NAME, - pageService - .getI18nSupport() - .getText(new LocTextKey("sebserver.form.validation.fieldError.notNull"))); - return false; - } else if (fieldValue.length() < 3 || fieldValue.length() > 255) { - form.setFieldError( - Domain.CONFIGURATION_NODE.ATTR_NAME, - pageService - .getI18nSupport() - .getText(new LocTextKey("sebserver.form.validation.fieldError.size", - null, - null, - null, - 3, - 255))); - return false; - } - - if (fieldControl != null && fieldControl instanceof FileUploadSelection) { - final FileUploadSelection fileUpload = (FileUploadSelection) fieldControl; - final InputStream inputStream = fileUpload.getInputStream(); - if (inputStream != null) { - final RestCall.RestCallBuilder restCall = (newConfig) - ? pageService.getRestService() - .getBuilder(ImportNewExamConfig.class) - : pageService.getRestService() - .getBuilder(ImportExamConfigOnExistingConfig.class); - - restCall - .withHeader( - API.IMPORT_PASSWORD_ATTR_NAME, - form.getFieldValue(API.IMPORT_PASSWORD_ATTR_NAME)) - .withBody(inputStream); - - if (newConfig) { - restCall - .withHeader( - Domain.CONFIGURATION_NODE.ATTR_NAME, - form.getFieldValue(Domain.CONFIGURATION_NODE.ATTR_NAME)) - .withHeader( - Domain.CONFIGURATION_NODE.ATTR_DESCRIPTION, - form.getFieldValue(Domain.CONFIGURATION_NODE.ATTR_DESCRIPTION)) - .withHeader( - Domain.CONFIGURATION_NODE.ATTR_TEMPLATE_ID, - form.getFieldValue(Domain.CONFIGURATION_NODE.ATTR_TEMPLATE_ID)); - } else { - restCall.withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId); - } - - final Result configuration = restCall - .call(); - - if (!configuration.hasError()) { - context.publishInfo(SebExamConfigPropForm.FORM_IMPORT_CONFIRM_TEXT_KEY); - if (newConfig) { - - final PageAction action = pageService.pageActionBuilder(context) - .newAction(ActionDefinition.SEB_EXAM_CONFIG_IMPORT_TO_NEW_CONFIG) - .create(); - - pageService.firePageEvent( - new ActionEvent(action), - action.pageContext()); - } - return true; - } else { - final Exception error = configuration.getError(); - if (error instanceof RestCallError) { - ((RestCallError) error) - .getErrorMessages() - .stream() - .findFirst() - .ifPresent(message -> { - if (APIMessage.ErrorMessage.MISSING_PASSWORD.isOf(message)) { - formHandle - .getContext() - .publishPageMessage(MISSING_PASSWORD); - } else { - formHandle - .getContext() - .notifyImportError(EntityType.CONFIGURATION_NODE, error); - } - }); - return true; - } - - formHandle.getContext().notifyError( - SebExamConfigPropForm.FORM_TITLE, - configuration.getError()); - - return true; - } - } else { - formHandle.getContext().publishPageMessage( - new LocTextKey("sebserver.error.unexpected"), - new LocTextKey("Please selecte a valid SEB Exam Configuration File")); - } - } - - return false; - } catch (final Exception e) { - formHandle.getContext().notifyError(SebExamConfigPropForm.FORM_TITLE, e); - return true; - } - } - - private static final class ImportFormContext implements ModalInputDialogComposer> { - - private final PageService pageService; - private final PageContext pageContext; - private final boolean newConfig; - - private Form form = null; - - protected ImportFormContext( - final PageService pageService, - final PageContext pageContext, - final boolean newConfig) { - - this.pageService = pageService; - this.pageContext = pageContext; - this.newConfig = newConfig; - } - - @Override - public Supplier> compose(final Composite parent) { - - final Composite grid = this.pageService.getWidgetFactory() - .createPopupScrollComposite(parent); - - final ResourceService resourceService = this.pageService.getResourceService(); - final List> examConfigTemplateResources = resourceService.getExamConfigTemplateResources(); - final FormHandle formHandle = this.pageService.formBuilder( - this.pageContext.copyOf(grid)) - .readonly(false) - .addField(FormBuilder.fileUpload( - API.IMPORT_FILE_ATTR_NAME, - SebExamConfigPropForm.FORM_IMPORT_SELECT_TEXT_KEY, - null, - API.SEB_FILE_EXTENSION)) - - .addFieldIf( - () -> this.newConfig, - () -> FormBuilder.text( - Domain.CONFIGURATION_NODE.ATTR_NAME, - SebExamConfigPropForm.FORM_NAME_TEXT_KEY)) - .addFieldIf( - () -> this.newConfig, - () -> FormBuilder.text( - Domain.CONFIGURATION_NODE.ATTR_DESCRIPTION, - SebExamConfigPropForm.FORM_DESCRIPTION_TEXT_KEY) - .asArea()) - .addFieldIf( - () -> this.newConfig && !examConfigTemplateResources.isEmpty(), - () -> FormBuilder.singleSelection( - Domain.CONFIGURATION_NODE.ATTR_TEMPLATE_ID, - SebExamConfigPropForm.FORM_TEMPLATE_TEXT_KEY, - null, - resourceService::getExamConfigTemplateResources)) - - .addField(FormBuilder.text( - API.IMPORT_PASSWORD_ATTR_NAME, - SebExamConfigPropForm.FORM_IMPORT_PASSWORD_TEXT_KEY, - "").asPasswordField()) - .build(); - - this.form = formHandle.getForm(); - return () -> formHandle; - } - - void cancelUpload() { - if (this.form != null) { - final Control fieldControl = this.form.getFieldControl(API.IMPORT_FILE_ATTR_NAME); - if (fieldControl != null && fieldControl instanceof FileUploadSelection) { - ((FileUploadSelection) fieldControl).close(); - } - } - } - } - -} +/* + * 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.content; + +import java.io.InputStream; +import java.util.List; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; + +import org.apache.commons.lang3.StringUtils; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; + +import ch.ethz.seb.sebserver.gbl.api.API; +import ch.ethz.seb.sebserver.gbl.api.APIMessage; +import ch.ethz.seb.sebserver.gbl.api.EntityType; +import ch.ethz.seb.sebserver.gbl.model.Domain; +import ch.ethz.seb.sebserver.gbl.model.EntityKey; +import ch.ethz.seb.sebserver.gbl.model.sebconfig.Configuration; +import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode; +import ch.ethz.seb.sebserver.gbl.util.Result; +import ch.ethz.seb.sebserver.gbl.util.Tuple; +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.PageMessageException; +import ch.ethz.seb.sebserver.gui.service.page.PageService; +import ch.ethz.seb.sebserver.gui.service.page.event.ActionEvent; +import ch.ethz.seb.sebserver.gui.service.page.impl.ModalInputDialog; +import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCallError; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.examconfig.ImportExamConfigOnExistingConfig; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.examconfig.ImportNewExamConfig; +import ch.ethz.seb.sebserver.gui.widget.FileUploadSelection; + +final class SebExamConfigImportPopup { + + private final static PageMessageException MISSING_PASSWORD = new PageMessageException( + new LocTextKey("sebserver.examconfig.action.import.missing-password")); + + static Function importFunction( + final PageService pageService, + final boolean newConfig) { + + return action -> { + + final ModalInputDialog> dialog = + new ModalInputDialog>( + action.pageContext().getParent().getShell(), + pageService.getWidgetFactory()) + .setLargeDialogWidth(); + + final ImportFormContext importFormContext = new ImportFormContext( + pageService, + action.pageContext(), + newConfig); + + dialog.open( + SebExamConfigPropForm.FORM_IMPORT_TEXT_KEY, + (Predicate>) formHandle -> doImport( + pageService, + formHandle, + newConfig), + importFormContext::cancelUpload, + importFormContext); + + return action; + }; + } + + private static final boolean doImport( + final PageService pageService, + final FormHandle formHandle, + final boolean newConfig) { + + try { + final Form form = formHandle.getForm(); + final EntityKey entityKey = formHandle.getContext().getEntityKey(); + final Control fieldControl = form.getFieldInput(API.IMPORT_FILE_ATTR_NAME); + final PageContext context = formHandle.getContext(); + + // Ad-hoc field validation + formHandle.process(name -> true, field -> field.resetError()); + final String fieldValue = form.getFieldValue(Domain.CONFIGURATION_NODE.ATTR_NAME); + if (StringUtils.isBlank(fieldValue)) { + form.setFieldError( + Domain.CONFIGURATION_NODE.ATTR_NAME, + pageService + .getI18nSupport() + .getText(new LocTextKey("sebserver.form.validation.fieldError.notNull"))); + return false; + } else if (fieldValue.length() < 3 || fieldValue.length() > 255) { + form.setFieldError( + Domain.CONFIGURATION_NODE.ATTR_NAME, + pageService + .getI18nSupport() + .getText(new LocTextKey("sebserver.form.validation.fieldError.size", + null, + null, + null, + 3, + 255))); + return false; + } + + if (fieldControl != null && fieldControl instanceof FileUploadSelection) { + final FileUploadSelection fileUpload = (FileUploadSelection) fieldControl; + final InputStream inputStream = fileUpload.getInputStream(); + if (inputStream != null) { + final RestCall.RestCallBuilder restCall = (newConfig) + ? pageService.getRestService() + .getBuilder(ImportNewExamConfig.class) + : pageService.getRestService() + .getBuilder(ImportExamConfigOnExistingConfig.class); + + restCall + .withHeader( + API.IMPORT_PASSWORD_ATTR_NAME, + form.getFieldValue(API.IMPORT_PASSWORD_ATTR_NAME)) + .withBody(inputStream); + + if (newConfig) { + restCall + .withHeader( + Domain.CONFIGURATION_NODE.ATTR_NAME, + form.getFieldValue(Domain.CONFIGURATION_NODE.ATTR_NAME)) + .withHeader( + Domain.CONFIGURATION_NODE.ATTR_DESCRIPTION, + form.getFieldValue(Domain.CONFIGURATION_NODE.ATTR_DESCRIPTION)) + .withHeader( + Domain.CONFIGURATION_NODE.ATTR_TEMPLATE_ID, + form.getFieldValue(Domain.CONFIGURATION_NODE.ATTR_TEMPLATE_ID)); + } else { + restCall.withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId); + } + + final Result configuration = restCall + .call(); + + if (!configuration.hasError()) { + context.publishInfo(SebExamConfigPropForm.FORM_IMPORT_CONFIRM_TEXT_KEY); + if (newConfig) { + + final PageAction action = pageService.pageActionBuilder(context) + .newAction(ActionDefinition.SEB_EXAM_CONFIG_IMPORT_TO_NEW_CONFIG) + .create(); + + pageService.firePageEvent( + new ActionEvent(action), + action.pageContext()); + } + return true; + } else { + final Exception error = configuration.getError(); + if (error instanceof RestCallError) { + ((RestCallError) error) + .getErrorMessages() + .stream() + .findFirst() + .ifPresent(message -> { + if (APIMessage.ErrorMessage.MISSING_PASSWORD.isOf(message)) { + formHandle + .getContext() + .publishPageMessage(MISSING_PASSWORD); + } else { + formHandle + .getContext() + .notifyImportError(EntityType.CONFIGURATION_NODE, error); + } + }); + return true; + } + + formHandle.getContext().notifyError( + SebExamConfigPropForm.FORM_TITLE, + configuration.getError()); + + return true; + } + } else { + formHandle.getContext().publishPageMessage( + new LocTextKey("sebserver.error.unexpected"), + new LocTextKey("Please selecte a valid SEB Exam Configuration File")); + } + } + + return false; + } catch (final Exception e) { + formHandle.getContext().notifyError(SebExamConfigPropForm.FORM_TITLE, e); + return true; + } + } + + private static final class ImportFormContext implements ModalInputDialogComposer> { + + private final PageService pageService; + private final PageContext pageContext; + private final boolean newConfig; + + private Form form = null; + + protected ImportFormContext( + final PageService pageService, + final PageContext pageContext, + final boolean newConfig) { + + this.pageService = pageService; + this.pageContext = pageContext; + this.newConfig = newConfig; + } + + @Override + public Supplier> compose(final Composite parent) { + + final Composite grid = this.pageService.getWidgetFactory() + .createPopupScrollComposite(parent); + + final ResourceService resourceService = this.pageService.getResourceService(); + final List> examConfigTemplateResources = resourceService.getExamConfigTemplateResources(); + final FormHandle formHandle = this.pageService.formBuilder( + this.pageContext.copyOf(grid)) + .readonly(false) + .addField(FormBuilder.fileUpload( + API.IMPORT_FILE_ATTR_NAME, + SebExamConfigPropForm.FORM_IMPORT_SELECT_TEXT_KEY, + null, + API.SEB_FILE_EXTENSION)) + + .addFieldIf( + () -> this.newConfig, + () -> FormBuilder.text( + Domain.CONFIGURATION_NODE.ATTR_NAME, + SebExamConfigPropForm.FORM_NAME_TEXT_KEY)) + .addFieldIf( + () -> this.newConfig, + () -> FormBuilder.text( + Domain.CONFIGURATION_NODE.ATTR_DESCRIPTION, + SebExamConfigPropForm.FORM_DESCRIPTION_TEXT_KEY) + .asArea()) + .addFieldIf( + () -> this.newConfig && !examConfigTemplateResources.isEmpty(), + () -> FormBuilder.singleSelection( + Domain.CONFIGURATION_NODE.ATTR_TEMPLATE_ID, + SebExamConfigPropForm.FORM_TEMPLATE_TEXT_KEY, + null, + resourceService::getExamConfigTemplateResources)) + + .addField(FormBuilder.text( + API.IMPORT_PASSWORD_ATTR_NAME, + SebExamConfigPropForm.FORM_IMPORT_PASSWORD_TEXT_KEY, + "").asPasswordField()) + .build(); + + this.form = formHandle.getForm(); + return () -> formHandle; + } + + void cancelUpload() { + if (this.form != null) { + final Control fieldControl = this.form.getFieldInput(API.IMPORT_FILE_ATTR_NAME); + if (fieldControl != null && fieldControl instanceof FileUploadSelection) { + ((FileUploadSelection) fieldControl).close(); + } + } + } + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/form/CheckboxFieldBuilder.java b/src/main/java/ch/ethz/seb/sebserver/gui/form/CheckboxFieldBuilder.java index e6fc0aa3..8c2ba7d5 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/form/CheckboxFieldBuilder.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/form/CheckboxFieldBuilder.java @@ -1,47 +1,54 @@ -/* - * 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 org.apache.commons.lang3.BooleanUtils; -import org.eclipse.swt.SWT; -import org.eclipse.swt.layout.GridData; -import org.eclipse.swt.widgets.Button; -import org.eclipse.swt.widgets.Composite; -import org.eclipse.swt.widgets.Label; - -import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; - -public class CheckboxFieldBuilder extends FieldBuilder { - - protected CheckboxFieldBuilder(final String name, final LocTextKey label, final String value) { - super(name, label, value); - } - - @Override - void build(final FormBuilder builder) { - final boolean readonly = builder.readonly || this.readonly; - final Label titleLabel = createTitleLabel(builder.formParent, builder, this); - final Composite fieldGrid = createFieldGrid(builder.formParent, this.spanInput); - final Button checkbox = builder.widgetFactory.buttonLocalized( - fieldGrid, - SWT.CHECK, - null, null); - - final GridData gridData = new GridData(SWT.FILL, SWT.TOP, true, true); - checkbox.setLayoutData(gridData); - checkbox.setSelection(BooleanUtils.toBoolean(this.value)); - - if (readonly) { - checkbox.setEnabled(false); - } - - builder.form.putField(this.name, titleLabel, checkbox); - } - -} +/* + * 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 org.apache.commons.lang3.BooleanUtils; +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; + +import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; + +import java.util.function.Consumer; + +public class CheckboxFieldBuilder extends FieldBuilder { + + protected CheckboxFieldBuilder( + final String name, + final LocTextKey label, + final String value) { + + super(name, label, value); + } + + @Override + void build(final FormBuilder builder) { + final boolean readonly = builder.readonly || this.readonly; + final Control titleLabel = createTitleLabel(builder.formParent, builder, this); + final Composite fieldGrid = createFieldGrid(builder.formParent, this.spanInput); + final Button checkbox = builder.widgetFactory.buttonLocalized( + fieldGrid, + SWT.CHECK, + null, null); + + final GridData gridData = new GridData(SWT.FILL, SWT.TOP, true, true); + checkbox.setLayoutData(gridData); + checkbox.setSelection(BooleanUtils.toBoolean(this.value)); + + if (readonly) { + checkbox.setEnabled(false); + } + + builder.form.putField(this.name, titleLabel, checkbox); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/form/FieldBuilder.java b/src/main/java/ch/ethz/seb/sebserver/gui/form/FieldBuilder.java index 36756331..6eeb226e 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/form/FieldBuilder.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/form/FieldBuilder.java @@ -1,238 +1,239 @@ -/* - * 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.function.BooleanSupplier; - -import org.apache.commons.lang3.StringUtils; -import org.eclipse.rap.rwt.RWT; -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.gui.service.i18n.LocTextKey; -import ch.ethz.seb.sebserver.gui.widget.WidgetFactory; -import ch.ethz.seb.sebserver.gui.widget.WidgetFactory.CustomVariant; - -public abstract class FieldBuilder { - - public static final LocTextKey MANDATORY_TEXT_KEY = new LocTextKey("sebserver.form.mandatory"); - public static final String TOOLTIP_KEY_SUFFIX_LABEL = ".tooltip"; - public static final String TOOLTIP_KEY_SUFFIX_LEFT = ".tooltip.left"; - public static final String TOOLTIP_KEY_SUFFIX_RIGHT = ".tooltip.right"; - - int spanLabel = -1; - int spanInput = -1; - int spanEmptyCell = -1; - int titleValign = SWT.TOP; - Boolean autoEmptyCellSeparation = null; - String group = null; - boolean readonly = false; - boolean visible = true; - String defaultLabel = null; - boolean isMandatory = false; - - final String name; - final LocTextKey label; - final LocTextKey tooltipLabel; - final LocTextKey tooltipKeyLeft; - final LocTextKey tooltipKeyRight; - final T value; - - protected FieldBuilder(final String name, final LocTextKey label, final T value) { - this.name = name; - this.label = label; - this.value = value; - this.tooltipLabel = (label != null) ? new LocTextKey(label.name + TOOLTIP_KEY_SUFFIX_LABEL) : null; - this.tooltipKeyLeft = (label != null) ? new LocTextKey(label.name + TOOLTIP_KEY_SUFFIX_LEFT) : null; - this.tooltipKeyRight = (label != null) ? new LocTextKey(label.name + TOOLTIP_KEY_SUFFIX_RIGHT) : null; - } - - public FieldBuilder withDefaultLabel(final String defaultLabel) { - this.defaultLabel = defaultLabel; - return this; - } - - public FieldBuilder withLabelSpan(final int span) { - this.spanLabel = span; - return this; - } - - public FieldBuilder mandatory() { - this.isMandatory = true; - return this; - } - - public FieldBuilder mandatory(final boolean mandatory) { - this.isMandatory = mandatory; - return this; - } - - public FieldBuilder withInputSpan(final int span) { - this.spanInput = span; - return this; - } - - public FieldBuilder withEmptyCellSpan(final int span) { - this.spanEmptyCell = span; - return this; - } - - public FieldBuilder withEmptyCellSeparation(final boolean separation) { - this.autoEmptyCellSeparation = separation; - return this; - } - - public FieldBuilder withGroup(final String group) { - this.group = group; - return this; - } - - public FieldBuilder readonly(final boolean readonly) { - this.readonly = readonly; - return this; - } - - public FieldBuilder visibleIf(final boolean visible) { - this.visible = visible; - return this; - } - - public FieldBuilder readonlyIf(final BooleanSupplier readonly) { - this.readonly = readonly != null && readonly.getAsBoolean(); - return this; - } - - abstract void build(FormBuilder builder); - - protected static Label createTitleLabel( - final Composite parent, - final FormBuilder builder, - final FieldBuilder fieldBuilder) { - - if (fieldBuilder.label == null) { - return null; - } - - final Composite infoGrid = new Composite(parent, SWT.NONE); - final GridLayout gridLayout = new GridLayout(4, false); - gridLayout.verticalSpacing = 0; - gridLayout.marginHeight = 0; - gridLayout.marginWidth = 0; - gridLayout.marginRight = 0; - infoGrid.setLayout(gridLayout); - final GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, true); - gridData.horizontalSpan = (fieldBuilder.spanLabel > 0) ? fieldBuilder.spanLabel : 1; - infoGrid.setLayoutData(gridData); - - if (fieldBuilder.tooltipKeyLeft != null && - StringUtils.isNotBlank(builder.i18nSupport.getText(fieldBuilder.tooltipKeyLeft, ""))) { - - final Label info = builder.widgetFactory.imageButton( - WidgetFactory.ImageIcon.HELP, - infoGrid, - fieldBuilder.tooltipKeyLeft); - info.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, false, false)); - } - - final boolean hasLabelTooltip = (fieldBuilder.tooltipLabel != null && - StringUtils.isNotBlank(builder.i18nSupport.getText(fieldBuilder.tooltipLabel, ""))); - - final Label label = labelLocalized( - builder.widgetFactory, - infoGrid, - fieldBuilder.label, - fieldBuilder.defaultLabel, - (hasLabelTooltip) ? fieldBuilder.tooltipLabel : null, - 1, - fieldBuilder.titleValign); - - if (fieldBuilder.isMandatory) { - final Label mandatory = builder.widgetFactory.imageButton( - WidgetFactory.ImageIcon.MANDATORY, - infoGrid, - MANDATORY_TEXT_KEY); - mandatory.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, false, false)); - } - - if (fieldBuilder.tooltipKeyRight != null && - StringUtils.isNotBlank(builder.i18nSupport.getText(fieldBuilder.tooltipKeyRight, ""))) { - - final Label info = builder.widgetFactory.imageButton( - WidgetFactory.ImageIcon.HELP, - infoGrid, - fieldBuilder.tooltipKeyRight); - info.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, false, false)); - } - - return label; - } - - public static Label labelLocalized( - final WidgetFactory widgetFactory, - final Composite parent, - final LocTextKey locTextKey, - final String defaultText, - final int hspan) { - - return labelLocalized(widgetFactory, parent, locTextKey, defaultText, null, hspan, SWT.CENTER); - } - - public static final Label labelLocalized( - final WidgetFactory widgetFactory, - final Composite parent, - final LocTextKey locTextKey, - final String defaultText, - final LocTextKey tooltipTextKey, - final int hspan, - final int verticalAlignment) { - - final LocTextKey labelKey = StringUtils.isNotBlank(defaultText) - ? new LocTextKey(defaultText) - : locTextKey; - - final Label label = widgetFactory.labelLocalized( - parent, - labelKey, - tooltipTextKey); - final GridData gridData = new GridData(SWT.LEFT, verticalAlignment, false, false, hspan, 1); - gridData.heightHint = FormBuilder.FORM_ROW_HEIGHT; - label.setLayoutData(gridData); - label.setData(RWT.CUSTOM_VARIANT, CustomVariant.TITLE_LABEL.key); - return label; - } - - public static Composite createFieldGrid(final Composite parent, final int hspan) { - final Composite fieldGrid = new Composite(parent, SWT.NONE); - final GridLayout gridLayout = new GridLayout(); - gridLayout.verticalSpacing = 0; - gridLayout.marginHeight = 0; - gridLayout.marginWidth = 0; - gridLayout.marginRight = 0; - fieldGrid.setLayout(gridLayout); - - final GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, true); - gridData.horizontalSpan = hspan; - fieldGrid.setLayoutData(gridData); - - return fieldGrid; - } - - public static Label createErrorLabel(final Composite innerGrid) { - final Label errorLabel = new Label(innerGrid, SWT.NONE); - final GridData gridData = new GridData(SWT.FILL, SWT.CENTER, true, true); - errorLabel.setLayoutData(gridData); - errorLabel.setVisible(false); - errorLabel.setData(RWT.CUSTOM_VARIANT, CustomVariant.ERROR.key); - return errorLabel; - } - +/* + * 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.function.BooleanSupplier; + +import org.apache.commons.lang3.StringUtils; +import org.eclipse.rap.rwt.RWT; +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.Control; +import org.eclipse.swt.widgets.Label; + +import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; +import ch.ethz.seb.sebserver.gui.widget.WidgetFactory; +import ch.ethz.seb.sebserver.gui.widget.WidgetFactory.CustomVariant; + +public abstract class FieldBuilder { + + public static final LocTextKey MANDATORY_TEXT_KEY = new LocTextKey("sebserver.form.mandatory"); + public static final String TOOLTIP_KEY_SUFFIX_LABEL = ".tooltip"; + public static final String TOOLTIP_KEY_SUFFIX_LEFT = ".tooltip.left"; + public static final String TOOLTIP_KEY_SUFFIX_RIGHT = ".tooltip.right"; + + int spanLabel = -1; + int spanInput = -1; + int spanEmptyCell = -1; + int titleValign = SWT.TOP; + Boolean autoEmptyCellSeparation = null; + String group = null; + boolean readonly = false; + boolean visible = true; + String defaultLabel = null; + boolean isMandatory = false; + + final String name; + final LocTextKey label; + final LocTextKey tooltipLabel; + final LocTextKey tooltipKeyLeft; + final LocTextKey tooltipKeyRight; + final T value; + + protected FieldBuilder(final String name, final LocTextKey label, final T value) { + this.name = name; + this.label = label; + this.value = value; + this.tooltipLabel = (label != null) ? new LocTextKey(label.name + TOOLTIP_KEY_SUFFIX_LABEL) : null; + this.tooltipKeyLeft = (label != null) ? new LocTextKey(label.name + TOOLTIP_KEY_SUFFIX_LEFT) : null; + this.tooltipKeyRight = (label != null) ? new LocTextKey(label.name + TOOLTIP_KEY_SUFFIX_RIGHT) : null; + } + + public FieldBuilder withDefaultLabel(final String defaultLabel) { + this.defaultLabel = defaultLabel; + return this; + } + + public FieldBuilder withLabelSpan(final int span) { + this.spanLabel = span; + return this; + } + + public FieldBuilder mandatory() { + this.isMandatory = true; + return this; + } + + public FieldBuilder mandatory(final boolean mandatory) { + this.isMandatory = mandatory; + return this; + } + + public FieldBuilder withInputSpan(final int span) { + this.spanInput = span; + return this; + } + + public FieldBuilder withEmptyCellSpan(final int span) { + this.spanEmptyCell = span; + return this; + } + + public FieldBuilder withEmptyCellSeparation(final boolean separation) { + this.autoEmptyCellSeparation = separation; + return this; + } + + public FieldBuilder withGroup(final String group) { + this.group = group; + return this; + } + + public FieldBuilder readonly(final boolean readonly) { + this.readonly = readonly; + return this; + } + + public FieldBuilder visibleIf(final boolean visible) { + this.visible = visible; + return this; + } + + public FieldBuilder readonlyIf(final BooleanSupplier readonly) { + this.readonly = readonly != null && readonly.getAsBoolean(); + return this; + } + + abstract void build(FormBuilder builder); + + protected static Control createTitleLabel( + final Composite parent, + final FormBuilder builder, + final FieldBuilder fieldBuilder) { + + if (fieldBuilder.label == null) { + return null; + } + + final Composite infoGrid = new Composite(parent, SWT.NONE); + final GridLayout gridLayout = new GridLayout(4, false); + gridLayout.verticalSpacing = 0; + gridLayout.marginHeight = 0; + gridLayout.marginWidth = 0; + gridLayout.marginRight = 0; + infoGrid.setLayout(gridLayout); + final GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, true); + gridData.horizontalSpan = (fieldBuilder.spanLabel > 0) ? fieldBuilder.spanLabel : 1; + infoGrid.setLayoutData(gridData); + + if (fieldBuilder.tooltipKeyLeft != null && + StringUtils.isNotBlank(builder.i18nSupport.getText(fieldBuilder.tooltipKeyLeft, ""))) { + + final Label info = builder.widgetFactory.imageButton( + WidgetFactory.ImageIcon.HELP, + infoGrid, + fieldBuilder.tooltipKeyLeft); + info.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, false, false)); + } + + final boolean hasLabelTooltip = (fieldBuilder.tooltipLabel != null && + StringUtils.isNotBlank(builder.i18nSupport.getText(fieldBuilder.tooltipLabel, ""))); + + final Label label = labelLocalized( + builder.widgetFactory, + infoGrid, + fieldBuilder.label, + fieldBuilder.defaultLabel, + (hasLabelTooltip) ? fieldBuilder.tooltipLabel : null, + 1, + fieldBuilder.titleValign); + + if (fieldBuilder.isMandatory) { + final Label mandatory = builder.widgetFactory.imageButton( + WidgetFactory.ImageIcon.MANDATORY, + infoGrid, + MANDATORY_TEXT_KEY); + mandatory.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, false, false)); + } + + if (fieldBuilder.tooltipKeyRight != null && + StringUtils.isNotBlank(builder.i18nSupport.getText(fieldBuilder.tooltipKeyRight, ""))) { + + final Label info = builder.widgetFactory.imageButton( + WidgetFactory.ImageIcon.HELP, + infoGrid, + fieldBuilder.tooltipKeyRight); + info.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, false, false)); + } + + return infoGrid; + } + + public static Label labelLocalized( + final WidgetFactory widgetFactory, + final Composite parent, + final LocTextKey locTextKey, + final String defaultText, + final int hspan) { + + return labelLocalized(widgetFactory, parent, locTextKey, defaultText, null, hspan, SWT.CENTER); + } + + public static Label labelLocalized( + final WidgetFactory widgetFactory, + final Composite parent, + final LocTextKey locTextKey, + final String defaultText, + final LocTextKey tooltipTextKey, + final int hspan, + final int verticalAlignment) { + + final LocTextKey labelKey = StringUtils.isNotBlank(defaultText) + ? new LocTextKey(defaultText) + : locTextKey; + + final Label label = widgetFactory.labelLocalized( + parent, + labelKey, + tooltipTextKey); + final GridData gridData = new GridData(SWT.LEFT, verticalAlignment, false, false, hspan, 1); + gridData.heightHint = FormBuilder.FORM_ROW_HEIGHT; + label.setLayoutData(gridData); + label.setData(RWT.CUSTOM_VARIANT, CustomVariant.TITLE_LABEL.key); + return label; + } + + public static Composite createFieldGrid(final Composite parent, final int hspan) { + final Composite fieldGrid = new Composite(parent, SWT.NONE); + final GridLayout gridLayout = new GridLayout(); + gridLayout.verticalSpacing = 0; + gridLayout.marginHeight = 0; + gridLayout.marginWidth = 0; + gridLayout.marginRight = 0; + fieldGrid.setLayout(gridLayout); + + final GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, true); + gridData.horizontalSpan = hspan; + fieldGrid.setLayoutData(gridData); + + return fieldGrid; + } + + public static Label createErrorLabel(final Composite innerGrid) { + final Label errorLabel = new Label(innerGrid, SWT.NONE); + final GridData gridData = new GridData(SWT.FILL, SWT.CENTER, true, true); + errorLabel.setLayoutData(gridData); + errorLabel.setVisible(false); + errorLabel.setData(RWT.CUSTOM_VARIANT, CustomVariant.ERROR.key); + return errorLabel; + } + } \ No newline at end of file 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 index 508310e4..595b26e2 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/form/FileUploadFieldBuilder.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/form/FileUploadFieldBuilder.java @@ -1,52 +1,53 @@ -/* - * 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 titleLabel = createTitleLabel(builder.formParent, builder, this); - final Composite fieldGrid = 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 = createErrorLabel(fieldGrid); - builder.form.putField(this.name, titleLabel, fileUpload, errorLabel); - builder.setFieldVisible(this.visible, this.name); - } - -} +/* + * 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.Control; +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 Control titleLabel = createTitleLabel(builder.formParent, builder, this); + final Composite fieldGrid = 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 = createErrorLabel(fieldGrid); + builder.form.putField(this.name, titleLabel, 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 fcd4b36f..b93dec48 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 @@ -1,530 +1,547 @@ -/* - * Copyright (c) 2018 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 java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.function.BiConsumer; -import java.util.function.Consumer; -import java.util.function.Predicate; - -import org.apache.commons.lang3.BooleanUtils; -import org.apache.commons.lang3.StringUtils; -import org.eclipse.rap.rwt.RWT; -import org.eclipse.swt.browser.Browser; -import org.eclipse.swt.graphics.Color; -import org.eclipse.swt.widgets.Button; -import org.eclipse.swt.widgets.Control; -import org.eclipse.swt.widgets.Label; -import org.eclipse.swt.widgets.Text; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.ObjectNode; - -import ch.ethz.seb.sebserver.gbl.Constants; -import ch.ethz.seb.sebserver.gbl.api.JSONMapper; -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.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; -import ch.ethz.seb.sebserver.gui.widget.WidgetFactory.CustomVariant; - -public final class Form implements FormBinding { - - private final JSONMapper jsonMapper; - private final ObjectNode objectRoot; - - private final Map staticValues = new LinkedHashMap<>(); - private final MultiValueMap formFields = new LinkedMultiValueMap<>(); - private final Map> groups = new LinkedHashMap<>(); - - Form(final JSONMapper jsonMapper) { - this.jsonMapper = jsonMapper; - this.objectRoot = this.jsonMapper.createObjectNode(); - } - - @Override - public String getFormAsJson() { - try { - flush(); - return this.jsonMapper.writeValueAsString(this.objectRoot); - } catch (final Exception e) { - throw new RuntimeException("Unexpected error while trying to create json form Form post: ", e); - } - } - - @Override - public String getFormUrlEncoded() { - final StringBuffer buffer = new StringBuffer(); - for (final Map.Entry entry : this.staticValues.entrySet()) { - appendFormUrlEncodedValue(buffer, entry.getKey(), entry.getValue()); - } - - this.formFields.entrySet() - .stream() - .forEach(entry -> { - entry.getValue() - .stream() - .filter(Form::valueApplicationFilter) - .forEach(ffa -> { - if (ffa.listValue) { - appendFormUrlEncodedValue( - buffer, - entry.getKey(), - ffa.getStringValue()); - } else { - appendFormUrlEncodedSingleValue( - buffer, - entry.getKey(), - ffa.getStringValue(), - false); - } - }); - }); - - return buffer.toString(); - } - - public void putStatic(final String name, final String value) { - if (StringUtils.isNotBlank(value)) { - this.staticValues.put(name, value); - } - } - - public void addToGroup(final String groupName, final String fieldName) { - if (this.formFields.containsKey(fieldName)) { - this.groups.computeIfAbsent(groupName, k -> new HashSet<>()) - .add(fieldName); - } - } - - public boolean hasFields() { - return !this.formFields.isEmpty(); - } - - public boolean hasField(final String fieldName) { - return this.formFields.containsKey(fieldName); - } - - Form putReadonlyField(final String name, final Label label, final Text field) { - this.formFields.add(name, createReadonlyAccessor(label, field)); - return this; - } - - Form putReadonlyField(final String name, final Label label, final Browser field) { - this.formFields.add(name, createReadonlyAccessor(label, field)); - return this; - } - - Form putField(final String name, final Label label, final Text field, final Label errorLabel) { - this.formFields.add(name, createAccessor(label, field, errorLabel)); - return this; - } - - Form putField(final String name, final Label label, final Button checkbox) { - this.formFields.add(name, createAccessor(label, checkbox, null)); - return this; - } - - void putField(final String name, final Label label, final Selection field, final Label errorLabel) { - this.formFields.add(name, createAccessor(label, field, errorLabel)); - } - - void putField(final String name, final Label label, final ThresholdList field, final Label errorLabel) { - this.formFields.add(name, createAccessor(label, field, 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) { - return null; - } - - 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) { - return; - } - - fieldAccessor.setStringValue(attributeValue); - } - - public void setFieldColor(final String attributeName, final Color color) { - final FormFieldAccessor fieldAccessor = this.formFields.getFirst(attributeName); - if (fieldAccessor == null) { - return; - } - - fieldAccessor.setBackgroundColor(color); - } - - public void setFieldTextColor(final String attributeName, final Color color) { - final FormFieldAccessor fieldAccessor = this.formFields.getFirst(attributeName); - if (fieldAccessor == null) { - return; - } - - fieldAccessor.setTextColor(color); - } - - public void allVisible() { - process( - Utils.truePredicate(), - ffa -> ffa.setVisible(true)); - } - - public void setVisible(final boolean visible, final String group) { - if (!this.groups.containsKey(group)) { - return; - } - - final Set namesSet = this.groups.get(group); - process( - name -> namesSet.contains(name), - ffa -> ffa.setVisible(visible)); - } - - public void setFieldVisible(final boolean visible, final String fieldName) { - final List list = this.formFields.get(fieldName); - if (list != null) { - list.stream().forEach(ffa -> ffa.setVisible(visible)); - } - } - - public boolean hasAnyError() { - return this.formFields.entrySet() - .stream() - .flatMap(entity -> entity.getValue().stream()) - .filter(a -> a.hasError) - .findFirst() - .isPresent(); - } - - public void clearErrors() { - process( - Utils.truePredicate(), - ffa -> ffa.resetError()); - } - - public void setFieldError(final String fieldName, final String errorMessage) { - final List list = this.formFields.get(fieldName); - if (list != null) { - list - .stream() - .forEach(ffa -> ffa.setError(errorMessage)); - } - } - - public void process( - final Predicate nameFilter, - final Consumer processor) { - - this.formFields.entrySet() - .stream() - .filter(entity -> nameFilter.test(entity.getKey())) - .flatMap(entity -> entity.getValue().stream()) - .forEach(processor); - } - - private void flush() { - this.objectRoot.removeAll(); - for (final Map.Entry entry : this.staticValues.entrySet()) { - final String value = entry.getValue(); - if (StringUtils.isNotBlank(value)) { - this.objectRoot.put(entry.getKey(), value); - } - } - - for (final Map.Entry> entry : this.formFields.entrySet()) { - entry.getValue() - .stream() - .filter(Form::valueApplicationFilter) - .forEach(ffa -> ffa.putJsonValue(entry.getKey(), this.objectRoot)); - } - } - - private static boolean valueApplicationFilter(final FormFieldAccessor ffa) { - return ffa.getStringValue() != null; - } - - // following are FormFieldAccessor implementations for all field types - //@formatter:off - private FormFieldAccessor createReadonlyAccessor(final Label label, final Text field) { - return new FormFieldAccessor(label, field, null) { - @Override public String getStringValue() { return null; } - @Override public void setStringValue(final String value) { field.setText( (value == null) ? StringUtils.EMPTY : value); } - }; - } - private FormFieldAccessor createReadonlyAccessor(final Label label, final Browser field) { - return new FormFieldAccessor(label, field, null) { - @Override public String getStringValue() { return null; } - @Override public void setStringValue(final String value) { field.setText( (value == null) ? StringUtils.EMPTY : value); } - }; - } - private FormFieldAccessor createAccessor(final Label label, final Text text, final Label errorLabel) { - return new FormFieldAccessor(label, text, errorLabel) { - @Override public String getStringValue() {return text.getText();} - @Override public void setStringValue(final String value) {text.setText(value);} - }; - } - private FormFieldAccessor createAccessor(final Label label, final Button checkbox, final Label errorLabel) { - return new FormFieldAccessor(label, checkbox, errorLabel) { - @Override public String getStringValue() {return BooleanUtils.toStringTrueFalse(checkbox.getSelection());} - @Override public void setStringValue(final String value) {checkbox.setSelection(BooleanUtils.toBoolean(value));} - }; - } - private FormFieldAccessor createAccessor(final Label label, final Selection selection, final Label errorLabel) { - switch (selection.type()) { - case MULTI: - case MULTI_COMBO: - case MULTI_CHECKBOX: - return createAccessor(label, selection, Form::adaptCommaSeparatedStringToJsonArray, errorLabel); - default : return createAccessor(label, selection, null, errorLabel); - } - } - private FormFieldAccessor createAccessor( - final Label label, - final Selection selection, - final BiConsumer, ObjectNode> jsonValueAdapter, - final Label errorLabel) { - - return new FormFieldAccessor( - label, - selection.adaptToControl(), - jsonValueAdapter, - selection.type() != Type.SINGLE, - errorLabel) { - @Override public String getStringValue() { return selection.getSelectionValue(); } - @Override public void setStringValue(final String value) { selection.select(value); } - }; - } - private FormFieldAccessor createAccessor(final Label label, final ThresholdList thresholdList, final Label errorLabel) { - return new FormFieldAccessor(label, thresholdList, null, true, errorLabel) { - @Override public String getStringValue() { - return ThresholdListBuilder - .thresholdsToFormURLEncodedStringValue(thresholdList.getThresholds()); - } - @Override - public void putJsonValue(final String key, final ObjectNode objectRoot) { - final Collection thresholds = thresholdList.getThresholds(); - if (thresholds == null || thresholds.isEmpty()) { - return; - } - - final ArrayNode array = Form.this.jsonMapper.valueToTree(thresholds); - objectRoot.putArray(key).addAll(array); - } - }; - } - 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 - - /* - * Adds the given name and value in from URL encoded format to the given StringBuffer. - * Checks first if the value String is a comma separated list. If true, splits values - * and adds every value within the same name mapping to the string buffer - */ - private static void appendFormUrlEncodedValue( - final StringBuffer buffer, - final String name, - final String value) { - - if (StringUtils.isBlank(value)) { - return; - } - - final String[] split = StringUtils.split(value, Constants.LIST_SEPARATOR_CHAR); - for (int i = 0; i < split.length; i++) { - appendFormUrlEncodedSingleValue(buffer, name, split[i], true); - } - } - - private static void appendFormUrlEncodedSingleValue( - final StringBuffer buffer, - final String name, - final String value, - final boolean checkMultiValue) { - - if (StringUtils.isBlank(value)) { - return; - } - - if (buffer.length() > 0) { - buffer.append(Constants.FORM_URL_ENCODED_SEPARATOR); - } - - // check of the string value is a name-value pair. If true, use the specified name an value - // otherwise use the general name given within this method call and - if (checkMultiValue && value.contains(Constants.FORM_URL_ENCODED_NAME_VALUE_SEPARATOR)) { - final String[] nameValue = StringUtils.split(value, Constants.FORM_URL_ENCODED_NAME_VALUE_SEPARATOR); - buffer.append(nameValue[0]) - .append(Constants.FORM_URL_ENCODED_NAME_VALUE_SEPARATOR) - .append(Utils.encodeFormURL_UTF_8(nameValue[1])); - } else { - buffer.append(name) - .append(Constants.FORM_URL_ENCODED_NAME_VALUE_SEPARATOR) - .append(Utils.encodeFormURL_UTF_8(value)); - } - } - - private static final void adaptCommaSeparatedStringToJsonArray( - final Tuple tuple, - final ObjectNode jsonNode) { - - if (StringUtils.isNotBlank(tuple._2)) { - final ArrayNode arrayNode = jsonNode.putArray(tuple._1); - final String[] split = StringUtils.split(tuple._2, Constants.LIST_SEPARATOR); - for (int i = 0; i < split.length; i++) { - arrayNode.add(split[i]); - } - } - } - - public static abstract class FormFieldAccessor { - - public final Label label; - public final Control control; - private final Label errorLabel; - private final BiConsumer, ObjectNode> jsonValueAdapter; - private boolean hasError; - private final boolean listValue; - - FormFieldAccessor(final Label label, final Control control, final Label errorLabel) { - this(label, control, null, false, errorLabel); - } - - FormFieldAccessor( - final Label label, - final Control control, - final BiConsumer, ObjectNode> jsonValueAdapter, - final boolean listValue, - final Label errorLabel) { - - this.label = label; - this.control = control; - this.errorLabel = errorLabel; - if (jsonValueAdapter != null) { - this.jsonValueAdapter = jsonValueAdapter; - } else { - this.jsonValueAdapter = (tuple, jsonObject) -> { - if (tuple._2 != null) { - jsonObject.put(tuple._1, tuple._2); - } - }; - } - this.listValue = listValue; - } - - abstract String getStringValue(); - - public void setStringValue(final String value) { - throw new UnsupportedOperationException(); - } - - public void setBackgroundColor(final Color color) { - if (this.control != null) { - this.control.setBackground(color); - } - } - - public void setTextColor(final Color color) { - if (this.control != null) { - this.control.setForeground(color); - } - } - - public void setVisible(final boolean visible) { - if (this.label != null) { - this.label.setVisible(visible); - } - this.control.setVisible(visible); - } - - public void putJsonValue(final String key, final ObjectNode objectRoot) { - this.jsonValueAdapter.accept(new Tuple<>(key, getStringValue()), objectRoot); - } - - public void setError(final String errorMessage) { - if (this.errorLabel == null) { - return; - } - - if (errorMessage == null) { - resetError(); - return; - } - - if (!this.hasError) { - this.control.setData(RWT.CUSTOM_VARIANT, CustomVariant.ERROR.key); - this.errorLabel.setText("- " + errorMessage); - this.errorLabel.setVisible(true); - this.hasError = true; - } - } - - public void resetError() { - if (this.errorLabel == null) { - return; - } - - if (this.hasError) { - this.control.setData(RWT.CUSTOM_VARIANT, null); - this.errorLabel.setVisible(false); - this.errorLabel.setText(StringUtils.EMPTY); - this.hasError = false; - } - } - } - -} +/* + * Copyright (c) 2018 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 java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Predicate; + +import ch.ethz.seb.sebserver.gbl.util.Cryptor; +import ch.ethz.seb.sebserver.gui.widget.PasswordInput; +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.StringUtils; +import org.eclipse.rap.rwt.RWT; +import org.eclipse.swt.browser.Browser; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Text; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import ch.ethz.seb.sebserver.gbl.Constants; +import ch.ethz.seb.sebserver.gbl.api.JSONMapper; +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.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; +import ch.ethz.seb.sebserver.gui.widget.WidgetFactory.CustomVariant; + +public final class Form implements FormBinding { + + private final Cryptor cryptor; + private final JSONMapper jsonMapper; + private final ObjectNode objectRoot; + + private final Map staticValues = new LinkedHashMap<>(); + private final MultiValueMap formFields = new LinkedMultiValueMap<>(); + private final Map> groups = new LinkedHashMap<>(); + + Form(final JSONMapper jsonMapper, final Cryptor cryptor) { + this.jsonMapper = jsonMapper; + this.cryptor = cryptor; + this.objectRoot = this.jsonMapper.createObjectNode(); + } + + @Override + public String getFormAsJson() { + try { + flush(); + return this.jsonMapper.writeValueAsString(this.objectRoot); + } catch (final Exception e) { + throw new RuntimeException("Unexpected error while trying to create json form Form post: ", e); + } + } + + @Override + public String getFormUrlEncoded() { + final StringBuffer buffer = new StringBuffer(); + for (final Map.Entry entry : this.staticValues.entrySet()) { + appendFormUrlEncodedValue(buffer, entry.getKey(), entry.getValue()); + } + + this.formFields.forEach((key, value) -> value + .stream() + .filter(Form::valueApplicationFilter) + .forEach(ffa -> { + if (ffa.listValue) { + appendFormUrlEncodedValue( + buffer, + key, + ffa.getStringValue()); + } else { + appendFormUrlEncodedSingleValue( + buffer, + key, + ffa.getStringValue(), + false); + } + })); + + return buffer.toString(); + } + + public void putStatic(final String name, final String value) { + if (StringUtils.isNotBlank(value)) { + this.staticValues.put(name, value); + } + } + + public void addToGroup(final String groupName, final String fieldName) { + if (this.formFields.containsKey(fieldName)) { + this.groups.computeIfAbsent(groupName, k -> new HashSet<>()) + .add(fieldName); + } + } + + public boolean hasFields() { + return !this.formFields.isEmpty(); + } + + public boolean hasField(final String fieldName) { + return this.formFields.containsKey(fieldName); + } + + Form putReadonlyField(final String name, final Control label, final Text field) { + this.formFields.add(name, createReadonlyAccessor(label, field)); + return this; + } + + Form putReadonlyField(final String name, final Control label, final Browser field) { + this.formFields.add(name, createReadonlyAccessor(label, field)); + return this; + } + + Form putField(final String name, final Control label, final Text field, final Label errorLabel) { + this.formFields.add(name, createAccessor(label, field, errorLabel)); + return this; + } + + Form putField(final String name, final Control label, final PasswordInput field, final Label errorLabel) { + this.formFields.add(name, createAccessor(label, field, errorLabel)); + return this; + } + + Form putField(final String name, final Control label, final Button checkbox) { + this.formFields.add(name, createAccessor(label, checkbox, null)); + return this; + } + + Form putField(final String name, final Control label, final Selection field, final Label errorLabel) { + this.formFields.add(name, createAccessor(label, field, errorLabel)); + return this; + } + + Form putField(final String name, final Control label, final ThresholdList field, final Label errorLabel) { + this.formFields.add(name, createAccessor(label, field, errorLabel)); + return this; + } + + Form putField(final String name, final Control label, final ImageUploadSelection imageUpload, + final Label errorLabel) { + final FormFieldAccessor createAccessor = createAccessor(label, imageUpload, errorLabel); + imageUpload.setErrorHandler(createAccessor::setError); + this.formFields.add(name, createAccessor); + return this; + } + + Form putField(final String name, final Control label, final FileUploadSelection fileUpload, final Label errorLabel) { + final FormFieldAccessor createAccessor = createAccessor(label, fileUpload, errorLabel); + fileUpload.setErrorHandler(createAccessor::setError); + this.formFields.add(name, createAccessor); + return this; + } + + public String getFieldValue(final String attributeName) { + final FormFieldAccessor fieldAccessor = this.formFields.getFirst(attributeName); + if (fieldAccessor == null) { + return null; + } + + return fieldAccessor.getStringValue(); + } + + public Control getFieldInput(final String attributeName) { + final FormFieldAccessor fieldAccessor = this.formFields.getFirst(attributeName); + if (fieldAccessor == null) { + return null; + } + + return fieldAccessor.input; + } + + public void setFieldValue(final String attributeName, final String attributeValue) { + final FormFieldAccessor fieldAccessor = this.formFields.getFirst(attributeName); + if (fieldAccessor == null) { + return; + } + + fieldAccessor.setStringValue(attributeValue); + } + + public void setFieldColor(final String attributeName, final Color color) { + final FormFieldAccessor fieldAccessor = this.formFields.getFirst(attributeName); + if (fieldAccessor == null) { + return; + } + + fieldAccessor.setBackgroundColor(color); + } + + public void setFieldTextColor(final String attributeName, final Color color) { + final FormFieldAccessor fieldAccessor = this.formFields.getFirst(attributeName); + if (fieldAccessor == null) { + return; + } + + fieldAccessor.setTextColor(color); + } + + public void allVisible() { + process( + Utils.truePredicate(), + ffa -> ffa.setVisible(true)); + } + + public void setVisible(final boolean visible, final String group) { + if (!this.groups.containsKey(group)) { + return; + } + + final Set namesSet = this.groups.get(group); + process( + namesSet::contains, + ffa -> ffa.setVisible(visible)); + } + + public void setFieldVisible(final boolean visible, final String fieldName) { + final List list = this.formFields.get(fieldName); + if (list != null) { + list.forEach(ffa -> ffa.setVisible(visible)); + } + } + + public boolean hasAnyError() { + return this.formFields.entrySet() + .stream() + .flatMap(entity -> entity.getValue().stream()) + .anyMatch(a -> a.hasError); + } + + public void clearErrors() { + process( + Utils.truePredicate(), + FormFieldAccessor::resetError); + } + + public void setFieldError(final String fieldName, final String errorMessage) { + final List list = this.formFields.get(fieldName); + if (list != null) { + list.forEach(ffa -> ffa.setError(errorMessage)); + } + } + + public void process( + final Predicate nameFilter, + final Consumer processor) { + + this.formFields.entrySet() + .stream() + .filter(entity -> nameFilter.test(entity.getKey())) + .flatMap(entity -> entity.getValue().stream()) + .forEach(processor); + } + + private void flush() { + this.objectRoot.removeAll(); + for (final Map.Entry entry : this.staticValues.entrySet()) { + final String value = entry.getValue(); + if (StringUtils.isNotBlank(value)) { + this.objectRoot.put(entry.getKey(), value); + } + } + + for (final Map.Entry> entry : this.formFields.entrySet()) { + entry.getValue() + .stream() + .filter(Form::valueApplicationFilter) + .forEach(ffa -> ffa.putJsonValue(entry.getKey(), this.objectRoot)); + } + } + + private static boolean valueApplicationFilter(final FormFieldAccessor ffa) { + return ffa.getStringValue() != null; + } + + // following are FormFieldAccessor implementations for all field types + //@formatter:off + private FormFieldAccessor createReadonlyAccessor(final Control label, final Text field) { + return new FormFieldAccessor(label, field, null) { + @Override public String getStringValue() { return null; } + @Override public void setStringValue(final String value) { field.setText( (value == null) ? StringUtils.EMPTY : value); } + }; + } + private FormFieldAccessor createReadonlyAccessor(final Control label, final Browser field) { + return new FormFieldAccessor(label, field, null) { + @Override public String getStringValue() { return null; } + @Override public void setStringValue(final String value) { field.setText( (value == null) ? StringUtils.EMPTY : value); } + }; + } + private FormFieldAccessor createAccessor(final Control label, final Text text, final Label errorLabel) { + return new FormFieldAccessor(label, text, errorLabel) { + @Override public String getStringValue() {return text.getText();} + @Override public void setStringValue(final String value) {text.setText(value);} + }; + } + private FormFieldAccessor createAccessor(final Control label, final PasswordInput pwdInput, final Label errorLabel) { + return new FormFieldAccessor(label, pwdInput, errorLabel) { + @Override public String getStringValue() {return pwdInput.getValue() != null ? pwdInput.getValue().toString() : null;} + @Override public void setStringValue(final String value) { + if (StringUtils.isNotBlank(value)) { + pwdInput.setValue(cryptor.decrypt(value)); + } else { + pwdInput.setValue(value); + } + } + }; + } + private FormFieldAccessor createAccessor(final Control label, final Button checkbox, final Label errorLabel) { + return new FormFieldAccessor(label, checkbox, errorLabel) { + @Override public String getStringValue() {return BooleanUtils.toStringTrueFalse(checkbox.getSelection());} + @Override public void setStringValue(final String value) {checkbox.setSelection(BooleanUtils.toBoolean(value));} + }; + } + private FormFieldAccessor createAccessor(final Control label, final Selection selection, final Label errorLabel) { + switch (selection.type()) { + case MULTI: + case MULTI_COMBO: + case MULTI_CHECKBOX: + return createAccessor(label, selection, Form::adaptCommaSeparatedStringToJsonArray, errorLabel); + default : return createAccessor(label, selection, null, errorLabel); + } + } + private FormFieldAccessor createAccessor( + final Control label, + final Selection selection, + final BiConsumer, ObjectNode> jsonValueAdapter, + final Label errorLabel) { + + return new FormFieldAccessor( + label, + selection.adaptToControl(), + jsonValueAdapter, + selection.type() != Type.SINGLE, + errorLabel) { + @Override public String getStringValue() { return selection.getSelectionValue(); } + @Override public void setStringValue(final String value) { selection.select(value); } + }; + } + private FormFieldAccessor createAccessor(final Control label, final ThresholdList thresholdList, final Label errorLabel) { + return new FormFieldAccessor(label, thresholdList, null, true, errorLabel) { + @Override public String getStringValue() { + return ThresholdListBuilder + .thresholdsToFormURLEncodedStringValue(thresholdList.getThresholds()); + } + @Override + public void putJsonValue(final String key, final ObjectNode objectRoot) { + final Collection thresholds = thresholdList.getThresholds(); + if (thresholds == null || thresholds.isEmpty()) { + return; + } + + final ArrayNode array = Form.this.jsonMapper.valueToTree(thresholds); + objectRoot.putArray(key).addAll(array); + } + }; + } + private FormFieldAccessor createAccessor(final Control label, final ImageUploadSelection imageUpload, final Label errorLabel) { + return new FormFieldAccessor(label, imageUpload, errorLabel) { + @Override public String getStringValue() { return imageUpload.getImageBase64(); } + }; + } + private FormFieldAccessor createAccessor(final Control label, final FileUploadSelection fileUpload, final Label errorLabel) { + return new FormFieldAccessor(label, fileUpload, errorLabel) { + @Override public String getStringValue() { return fileUpload.getFileName(); } + }; + } + //@formatter:on + + /* + * Adds the given name and value in from URL encoded format to the given StringBuffer. + * Checks first if the value String is a comma separated list. If true, splits values + * and adds every value within the same name mapping to the string buffer + */ + private static void appendFormUrlEncodedValue( + final StringBuffer buffer, + final String name, + final String value) { + + if (StringUtils.isBlank(value)) { + return; + } + + final String[] split = StringUtils.split(value, Constants.LIST_SEPARATOR_CHAR); + for (int i = 0; i < split.length; i++) { + appendFormUrlEncodedSingleValue(buffer, name, split[i], true); + } + } + + private static void appendFormUrlEncodedSingleValue( + final StringBuffer buffer, + final String name, + final String value, + final boolean checkMultiValue) { + + if (StringUtils.isBlank(value)) { + return; + } + + if (buffer.length() > 0) { + buffer.append(Constants.FORM_URL_ENCODED_SEPARATOR); + } + + // check of the string value is a name-value pair. If true, use the specified name an value + // otherwise use the general name given within this method call and + if (checkMultiValue && value.contains(Constants.FORM_URL_ENCODED_NAME_VALUE_SEPARATOR)) { + final String[] nameValue = StringUtils.split(value, Constants.FORM_URL_ENCODED_NAME_VALUE_SEPARATOR); + buffer.append(nameValue[0]) + .append(Constants.FORM_URL_ENCODED_NAME_VALUE_SEPARATOR) + .append(Utils.encodeFormURL_UTF_8(nameValue[1])); + } else { + buffer.append(name) + .append(Constants.FORM_URL_ENCODED_NAME_VALUE_SEPARATOR) + .append(Utils.encodeFormURL_UTF_8(value)); + } + } + + private static void adaptCommaSeparatedStringToJsonArray( + final Tuple tuple, + final ObjectNode jsonNode) { + + if (StringUtils.isNotBlank(tuple._2)) { + final ArrayNode arrayNode = jsonNode.putArray(tuple._1); + final String[] split = StringUtils.split(tuple._2, Constants.LIST_SEPARATOR); + for (int i = 0; i < split.length; i++) { + arrayNode.add(split[i]); + } + } + } + + public static abstract class FormFieldAccessor { + + public final Control label; + public final Control input; + private final Label errorLabel; + private final BiConsumer, ObjectNode> jsonValueAdapter; + private boolean hasError; + private final boolean listValue; + + FormFieldAccessor(final Control label, final Control control, final Label errorLabel) { + this(label, control, null, false, errorLabel); + } + + FormFieldAccessor( + final Control label, + final Control input, + final BiConsumer, ObjectNode> jsonValueAdapter, + final boolean listValue, + final Label errorLabel) { + + this.label = label; + this.input = input; + this.errorLabel = errorLabel; + if (jsonValueAdapter != null) { + this.jsonValueAdapter = jsonValueAdapter; + } else { + this.jsonValueAdapter = (tuple, jsonObject) -> { + if (tuple._2 != null) { + jsonObject.put(tuple._1, tuple._2); + } + }; + } + this.listValue = listValue; + } + + abstract String getStringValue(); + + public void setStringValue(final String value) { + throw new UnsupportedOperationException(); + } + + public void setBackgroundColor(final Color color) { + if (this.input != null) { + this.input.setBackground(color); + } + } + + public void setTextColor(final Color color) { + if (this.input != null) { + this.input.setForeground(color); + } + } + + public void setVisible(final boolean visible) { + if (this.label != null) { + this.label.setVisible(visible); + } + this.input.setVisible(visible); + } + + public void putJsonValue(final String key, final ObjectNode objectRoot) { + this.jsonValueAdapter.accept(new Tuple<>(key, getStringValue()), objectRoot); + } + + public void setError(final String errorMessage) { + if (this.errorLabel == null) { + return; + } + + if (errorMessage == null) { + resetError(); + return; + } + + if (!this.hasError) { + this.input.setData(RWT.CUSTOM_VARIANT, CustomVariant.ERROR.key); + this.errorLabel.setText("- " + errorMessage); + this.errorLabel.setVisible(true); + this.hasError = true; + } + } + + public void resetError() { + if (this.errorLabel == null) { + return; + } + + if (this.hasError) { + this.input.setData(RWT.CUSTOM_VARIANT, null); + this.errorLabel.setVisible(false); + this.errorLabel.setText(StringUtils.EMPTY); + this.hasError = false; + } + } + } + +} 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 4baf55a8..dd988d18 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 @@ -1,294 +1,306 @@ -/* - * Copyright (c) 2018 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.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.function.BooleanSupplier; -import java.util.function.Supplier; - -import org.apache.commons.lang3.StringUtils; -import org.eclipse.swt.SWT; -import org.eclipse.swt.layout.GridData; -import org.eclipse.swt.widgets.Composite; -import org.eclipse.swt.widgets.Label; -import org.eclipse.swt.widgets.TabItem; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import ch.ethz.seb.sebserver.gbl.model.Entity; -import ch.ethz.seb.sebserver.gbl.model.exam.Indicator; -import ch.ethz.seb.sebserver.gbl.util.Tuple; -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.page.PageContext; -import ch.ethz.seb.sebserver.gui.service.page.PageService; -import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall; -import ch.ethz.seb.sebserver.gui.widget.Selection; -import ch.ethz.seb.sebserver.gui.widget.WidgetFactory; - -public class FormBuilder { - - public static final int FORM_ROW_HEIGHT = 25; - - private static final Logger log = LoggerFactory.getLogger(FormBuilder.class); - - final I18nSupport i18nSupport; - final PageService pageService; - final WidgetFactory widgetFactory; - public final PageContext pageContext; - public final Composite formParent; - public final Form form; - - boolean readonly = false; - private int defaultSpanLabel = 2; - private int defaultSpanInput = 5; - private int defaultSpanEmptyCell = 1; - private boolean emptyCellSeparation = true; - - public FormBuilder( - final PageService pageService, - final PageContext pageContext, - final int rows) { - - this.i18nSupport = pageService.getI18nSupport(); - this.pageService = pageService; - this.widgetFactory = pageService.getWidgetFactory(); - this.pageContext = pageContext; - this.form = new Form(pageService.getJSONMapper()); - - this.formParent = this.widgetFactory.formGrid( - pageContext.getParent(), - rows); - } - - public FormBuilder readonly(final boolean readonly) { - this.readonly = readonly; - return this; - } - - public FormBuilder setVisible(final boolean visible, final String group) { - this.form.setVisible(visible, group); - return this; - } - - public void setFieldVisible(final boolean visible, final String fieldName) { - this.form.setFieldVisible(visible, fieldName); - - } - - public FormBuilder setControl(final TabItem instTab) { - instTab.setControl(this.formParent); - return this; - } - - public FormBuilder withDefaultSpanLabel(final int span) { - this.defaultSpanLabel = span; - return this; - } - - public FormBuilder withDefaultSpanInput(final int span) { - this.defaultSpanInput = span; - return this; - } - - public FormBuilder withDefaultSpanEmptyCell(final int span) { - this.defaultSpanEmptyCell = span; - return this; - } - - public FormBuilder withEmptyCellSeparation(final boolean separation) { - this.emptyCellSeparation = separation; - return this; - } - - public FormBuilder addEmptyCellIf(final BooleanSupplier condition) { - if (condition != null && condition.getAsBoolean()) { - return addEmptyCell(); - } - return this; - } - - public FormBuilder addEmptyCell() { - return addEmptyCell(1); - } - - public FormBuilder addEmptyCell(final int span) { - empty(this.formParent, span, 1); - return this; - } - - public FormBuilder putStaticValueIf(final BooleanSupplier condition, final String name, final String value) { - if (condition != null && condition.getAsBoolean()) { - return putStaticValue(name, value); - } - - return this; - } - - public FormBuilder putStaticValue(final String name, final String value) { - try { - this.form.putStatic(name, value); - } catch (final Exception e) { - log.error("Failed to put static field value to json object: ", e); - } - return this; - } - - public FormBuilder addFieldIf( - final BooleanSupplier condition, - final Supplier> templateSupplier) { - - if (condition.getAsBoolean()) { - return addField(templateSupplier.get()); - } - - return this; - } - - public FormBuilder addField(final FieldBuilder template) { - template.spanLabel = (template.spanLabel < 0) ? this.defaultSpanLabel : template.spanLabel; - template.spanInput = (template.spanInput < 0) ? this.defaultSpanInput : template.spanInput; - template.spanEmptyCell = (template.spanEmptyCell < 0) ? this.defaultSpanEmptyCell : template.spanEmptyCell; - template.autoEmptyCellSeparation = (template.autoEmptyCellSeparation != null) - ? template.autoEmptyCellSeparation - : this.emptyCellSeparation; - - if (template.autoEmptyCellSeparation && this.form.hasFields()) { - addEmptyCell(template.spanEmptyCell); - } - - template.build(this); - - if (StringUtils.isNotBlank(template.group)) { - this.form.addToGroup(template.group, template.name); - } - - return this; - } - - public FormHandle build() { - return buildFor(null); - } - - public FormHandle buildFor(final RestCall post) { - return new FormHandle<>( - this.pageService, - this.pageContext, - this.form, - post); - } - - private void empty(final Composite parent, final int hspan, final int vspan) { - final Label empty = new Label(parent, SWT.LEFT); - final GridData gridData = new GridData(SWT.LEFT, SWT.TOP, false, false, hspan, vspan); - gridData.minimumWidth = 0; - gridData.widthHint = 0; - empty.setLayoutData(gridData); - } - - public static CheckboxFieldBuilder checkbox(final String name, final LocTextKey label) { - return new CheckboxFieldBuilder(name, label, null); - } - - public static CheckboxFieldBuilder checkbox(final String name, final LocTextKey label, final String value) { - return new CheckboxFieldBuilder(name, label, value); - } - - public static TextFieldBuilder text(final String name) { - return new TextFieldBuilder(name, null, null); - } - - public static TextFieldBuilder text(final String name, final LocTextKey label) { - return new TextFieldBuilder(name, label, null); - } - - public static TextFieldBuilder text(final String name, final LocTextKey label, final String value) { - return new TextFieldBuilder(name, label, value); - } - - public static TextFieldBuilder text(final String name, final LocTextKey label, - final Supplier valueSupplier) { - return new TextFieldBuilder(name, label, valueSupplier.get()); - } - - public static SelectionFieldBuilder singleSelection( - final String name, - final LocTextKey label, - final String value, - final Supplier>> itemsSupplier) { - - return new SelectionFieldBuilder(Selection.Type.SINGLE, name, label, value, itemsSupplier); - } - - public static SelectionFieldBuilder multiSelection( - final String name, - final LocTextKey label, - final String value, - final Supplier>> itemsSupplier) { - - return new SelectionFieldBuilder(Selection.Type.MULTI, name, label, value, itemsSupplier); - } - - public static SelectionFieldBuilder multiCheckboxSelection( - final String name, - final LocTextKey label, - final String value, - final Supplier>> itemsSupplier) { - - return new SelectionFieldBuilder(Selection.Type.MULTI_CHECKBOX, name, label, value, itemsSupplier); - } - - public static SelectionFieldBuilder multiComboSelection( - final String name, - final LocTextKey label, - final String value, - final Supplier>> itemsSupplier) { - - return new SelectionFieldBuilder(Selection.Type.MULTI_COMBO, name, label, value, itemsSupplier); - } - - public static SelectionFieldBuilder colorSelection( - final String name, - final LocTextKey label, - final String value) { - - return new SelectionFieldBuilder(Selection.Type.COLOR, name, label, value, null); - } - - public static ThresholdListBuilder thresholdList( - final String name, - final LocTextKey label, - final Indicator indicator) { - - return new ThresholdListBuilder( - name, - label, - indicator.thresholds); - } - - public static ImageUploadFieldBuilder imageUpload(final String name, final LocTextKey label, final String 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()); - } - -} +/* + * Copyright (c) 2018 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.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.function.BooleanSupplier; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import ch.ethz.seb.sebserver.gbl.util.Cryptor; +import org.apache.commons.lang3.StringUtils; +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.TabItem; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ch.ethz.seb.sebserver.gbl.model.Entity; +import ch.ethz.seb.sebserver.gbl.model.exam.Indicator; +import ch.ethz.seb.sebserver.gbl.util.Tuple; +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.page.PageContext; +import ch.ethz.seb.sebserver.gui.service.page.PageService; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall; +import ch.ethz.seb.sebserver.gui.widget.Selection; +import ch.ethz.seb.sebserver.gui.widget.WidgetFactory; + +public class FormBuilder { + + public static final int FORM_ROW_HEIGHT = 25; + + private static final Logger log = LoggerFactory.getLogger(FormBuilder.class); + + final Cryptor cryptor; + final I18nSupport i18nSupport; + final PageService pageService; + final WidgetFactory widgetFactory; + public final PageContext pageContext; + public final Composite formParent; + public final Form form; + + boolean readonly = false; + private int defaultSpanLabel = 2; + private int defaultSpanInput = 5; + private int defaultSpanEmptyCell = 1; + private boolean emptyCellSeparation = true; + + public FormBuilder( + final PageService pageService, + final PageContext pageContext, + final Cryptor cryptor, + final int rows) { + + this.cryptor = cryptor; + this.i18nSupport = pageService.getI18nSupport(); + this.pageService = pageService; + this.widgetFactory = pageService.getWidgetFactory(); + this.pageContext = pageContext; + this.form = new Form(pageService.getJSONMapper(), cryptor); + + this.formParent = this.widgetFactory.formGrid( + pageContext.getParent(), + rows); + } + + public FormBuilder readonly(final boolean readonly) { + this.readonly = readonly; + return this; + } + + public FormBuilder setVisible(final boolean visible, final String group) { + this.form.setVisible(visible, group); + return this; + } + + public void setFieldVisible(final boolean visible, final String fieldName) { + this.form.setFieldVisible(visible, fieldName); + + } + + public FormBuilder setControl(final TabItem instTab) { + instTab.setControl(this.formParent); + return this; + } + + public FormBuilder withDefaultSpanLabel(final int span) { + this.defaultSpanLabel = span; + return this; + } + + public FormBuilder withDefaultSpanInput(final int span) { + this.defaultSpanInput = span; + return this; + } + + public FormBuilder withDefaultSpanEmptyCell(final int span) { + this.defaultSpanEmptyCell = span; + return this; + } + + public FormBuilder withEmptyCellSeparation(final boolean separation) { + this.emptyCellSeparation = separation; + return this; + } + + public FormBuilder addEmptyCellIf(final BooleanSupplier condition) { + if (condition != null && condition.getAsBoolean()) { + return addEmptyCell(); + } + return this; + } + + public FormBuilder addEmptyCell() { + return addEmptyCell(1); + } + + public FormBuilder addEmptyCell(final int span) { + empty(this.formParent, span, 1); + return this; + } + + public FormBuilder putStaticValueIf(final BooleanSupplier condition, final String name, final String value) { + if (condition != null && condition.getAsBoolean()) { + return putStaticValue(name, value); + } + + return this; + } + + public FormBuilder putStaticValue(final String name, final String value) { + try { + this.form.putStatic(name, value); + } catch (final Exception e) { + log.error("Failed to put static field value to json object: ", e); + } + return this; + } + + public FormBuilder addFieldIf( + final BooleanSupplier condition, + final Supplier> templateSupplier) { + + if (condition.getAsBoolean()) { + return addField(templateSupplier.get()); + } + + return this; + } + + public FormBuilder addField(final FieldBuilder template) { + template.spanLabel = (template.spanLabel < 0) ? this.defaultSpanLabel : template.spanLabel; + template.spanInput = (template.spanInput < 0) ? this.defaultSpanInput : template.spanInput; + template.spanEmptyCell = (template.spanEmptyCell < 0) ? this.defaultSpanEmptyCell : template.spanEmptyCell; + template.autoEmptyCellSeparation = (template.autoEmptyCellSeparation != null) + ? template.autoEmptyCellSeparation + : this.emptyCellSeparation; + + if (template.autoEmptyCellSeparation && this.form.hasFields()) { + addEmptyCell(template.spanEmptyCell); + } + + template.build(this); + + if (StringUtils.isNotBlank(template.group)) { + this.form.addToGroup(template.group, template.name); + } + + return this; + } + + public FormHandle build() { + return buildFor(null); + } + + public FormHandle buildFor(final RestCall post) { + return new FormHandle<>( + this.pageService, + this.pageContext, + this.form, + post); + } + + private void empty(final Composite parent, final int hspan, final int vspan) { + final Label empty = new Label(parent, SWT.LEFT); + final GridData gridData = new GridData(SWT.LEFT, SWT.TOP, false, false, hspan, vspan); + gridData.minimumWidth = 0; + gridData.widthHint = 0; + empty.setLayoutData(gridData); + } + + public static CheckboxFieldBuilder checkbox(final String name, final LocTextKey label) { + return new CheckboxFieldBuilder(name, label, null); + } + + public static CheckboxFieldBuilder checkbox(final String name, final LocTextKey label, final String value) { + return new CheckboxFieldBuilder(name, label, value); + } + + public static TextFieldBuilder text(final String name) { + return new TextFieldBuilder(name, null, null); + } + + public static TextFieldBuilder text(final String name, final LocTextKey label) { + return new TextFieldBuilder(name, label, null); + } + + public static TextFieldBuilder text(final String name, final LocTextKey label, final String value) { + return new TextFieldBuilder(name, label, value); + } + + public static TextFieldBuilder text( + final String name, + final LocTextKey label, + final Supplier valueSupplier) { + + return new TextFieldBuilder(name, label, valueSupplier.get()); + } + + public static PasswordFieldBuilder password(final String name, final LocTextKey label, final CharSequence value) { + return new PasswordFieldBuilder(name, label, value); + } + + public static SelectionFieldBuilder singleSelection( + final String name, + final LocTextKey label, + final String value, + final Supplier>> itemsSupplier) { + + return new SelectionFieldBuilder(Selection.Type.SINGLE, name, label, value, itemsSupplier); + } + + public static SelectionFieldBuilder multiSelection( + final String name, + final LocTextKey label, + final String value, + final Supplier>> itemsSupplier) { + + return new SelectionFieldBuilder(Selection.Type.MULTI, name, label, value, itemsSupplier); + } + + public static SelectionFieldBuilder multiCheckboxSelection( + final String name, + final LocTextKey label, + final String value, + final Supplier>> itemsSupplier) { + + return new SelectionFieldBuilder(Selection.Type.MULTI_CHECKBOX, name, label, value, itemsSupplier); + } + + public static SelectionFieldBuilder multiComboSelection( + final String name, + final LocTextKey label, + final String value, + final Supplier>> itemsSupplier) { + + return new SelectionFieldBuilder(Selection.Type.MULTI_COMBO, name, label, value, itemsSupplier); + } + + public static SelectionFieldBuilder colorSelection( + final String name, + final LocTextKey label, + final String value) { + + return new SelectionFieldBuilder(Selection.Type.COLOR, name, label, value, null); + } + + public static ThresholdListBuilder thresholdList( + final String name, + final LocTextKey label, + final Indicator indicator) { + + return new ThresholdListBuilder( + name, + label, + indicator.thresholds); + } + + public static ImageUploadFieldBuilder imageUpload(final String name, final LocTextKey label, final String 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()); + } + +} 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 c43d3ac2..a19286be 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 @@ -1,57 +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 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.ImageUploadSelection; - -public final class ImageUploadFieldBuilder extends FieldBuilder { - - private int maxWidth = 100; - private int maxHeight = 100; - - ImageUploadFieldBuilder(final String name, final LocTextKey label, final String value) { - super(name, label, value); - } - - public ImageUploadFieldBuilder withMaxWidth(final int width) { - this.maxWidth = width; - return this; - } - - public ImageUploadFieldBuilder withMaxHeight(final int height) { - this.maxHeight = height; - return this; - } - - @Override - void build(final FormBuilder builder) { - final Label titleLabel = createTitleLabel(builder.formParent, builder, this); - final Composite fieldGrid = createFieldGrid(builder.formParent, this.spanInput); - final ImageUploadSelection imageUpload = builder.widgetFactory.imageUploadLocalized( - fieldGrid, - new LocTextKey("sebserver.overall.upload"), - builder.readonly || this.readonly, - this.maxWidth, - this.maxHeight); - final GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, false); - imageUpload.setLayoutData(gridData); - imageUpload.setImageBase64(this.value); - - final Label errorLabel = createErrorLabel(fieldGrid); - builder.form.putField(this.name, titleLabel, imageUpload, errorLabel); - builder.setFieldVisible(this.visible, this.name); - } - +/* + * 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 org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; + +import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; +import ch.ethz.seb.sebserver.gui.widget.ImageUploadSelection; + +public final class ImageUploadFieldBuilder extends FieldBuilder { + + private int maxWidth = 100; + private int maxHeight = 100; + + ImageUploadFieldBuilder(final String name, final LocTextKey label, final String value) { + super(name, label, value); + } + + public ImageUploadFieldBuilder withMaxWidth(final int width) { + this.maxWidth = width; + return this; + } + + public ImageUploadFieldBuilder withMaxHeight(final int height) { + this.maxHeight = height; + return this; + } + + @Override + void build(final FormBuilder builder) { + final Control titleLabel = createTitleLabel(builder.formParent, builder, this); + final Composite fieldGrid = createFieldGrid(builder.formParent, this.spanInput); + final ImageUploadSelection imageUpload = builder.widgetFactory.imageUploadLocalized( + fieldGrid, + new LocTextKey("sebserver.overall.upload"), + builder.readonly || this.readonly, + this.maxWidth, + this.maxHeight); + final GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, false); + imageUpload.setLayoutData(gridData); + imageUpload.setImageBase64(this.value); + + final Label errorLabel = createErrorLabel(fieldGrid); + builder.form.putField(this.name, titleLabel, imageUpload, errorLabel); + builder.setFieldVisible(this.visible, this.name); + } + } \ No newline at end of file diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/form/PasswordFieldBuilder.java b/src/main/java/ch/ethz/seb/sebserver/gui/form/PasswordFieldBuilder.java new file mode 100644 index 00000000..4aa310c1 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/form/PasswordFieldBuilder.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2020 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 ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; +import ch.ethz.seb.sebserver.gui.widget.PasswordInput; +import org.apache.commons.lang3.StringUtils; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; + +public class PasswordFieldBuilder extends FieldBuilder { + + PasswordFieldBuilder(final String name, final LocTextKey label, final CharSequence value) { + super(name, label, value); + } + + @Override + void build(FormBuilder builder) { + final boolean readonly = builder.readonly || this.readonly; + final Control titleLabel = createTitleLabel(builder.formParent, builder, this); + final Composite fieldGrid = createFieldGrid(builder.formParent, this.spanInput); + + final PasswordInput input = new PasswordInput(fieldGrid, builder.widgetFactory); + input.setEditable(!readonly); + input.setValue((StringUtils.isNotBlank(this.value)) + ? builder.cryptor.decrypt(this.value) + : this.value); + + final Label errorLabel = createErrorLabel(fieldGrid); + builder.form.putField(this.name, titleLabel, input, errorLabel); + } +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/form/SelectionFieldBuilder.java b/src/main/java/ch/ethz/seb/sebserver/gui/form/SelectionFieldBuilder.java index bb87a473..400638bd 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/form/SelectionFieldBuilder.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/form/SelectionFieldBuilder.java @@ -1,150 +1,150 @@ -/* - * 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.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.function.Consumer; -import java.util.function.Supplier; - -import org.apache.commons.lang3.StringUtils; -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.Control; -import org.eclipse.swt.widgets.Label; -import org.eclipse.swt.widgets.Text; - -import ch.ethz.seb.sebserver.gbl.Constants; -import ch.ethz.seb.sebserver.gbl.util.Tuple; -import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; -import ch.ethz.seb.sebserver.gui.service.i18n.PolyglotPageService; -import ch.ethz.seb.sebserver.gui.widget.Selection; -import ch.ethz.seb.sebserver.gui.widget.Selection.Type; - -public final class SelectionFieldBuilder extends FieldBuilder { - - final Supplier>> itemsSupplier; - Consumer
selectionListener = null; - final Selection.Type type; - - SelectionFieldBuilder( - final Selection.Type type, - final String name, - final LocTextKey label, - final String value, - final Supplier>> itemsSupplier) { - - super(name, label, value); - this.type = type; - this.itemsSupplier = itemsSupplier; - } - - public SelectionFieldBuilder withSelectionListener(final Consumer selectionListener) { - this.selectionListener = selectionListener; - return this; - } - - @Override - void build(final FormBuilder builder) { - final Label titleLabel = createTitleLabel(builder.formParent, builder, this); - - if (builder.readonly || this.readonly) { - buildReadOnly(builder, titleLabel); - } else { - buildInput(builder, titleLabel); - } - } - - private void buildInput(final FormBuilder builder, final Label titleLabel) { - - final Composite fieldGrid = createFieldGrid(builder.formParent, this.spanInput); - final String actionKey = (this.label != null) ? this.label.name + ".action" : null; - final Selection selection = builder.widgetFactory.selectionLocalized( - this.type, - fieldGrid, - this.itemsSupplier, - null, - null, - actionKey); - - final GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, false); - ((Control) selection).setLayoutData(gridData); - selection.select(this.value); - - final Label errorLabel = createErrorLabel(fieldGrid); - builder.form.putField(this.name, titleLabel, selection, errorLabel); - - if (this.selectionListener != null) { - ((Control) selection).addListener(SWT.Selection, e -> { - this.selectionListener.accept(builder.form); - }); - } - - builder.setFieldVisible(this.visible, this.name); - } - - /* Build the read-only representation of the selection field */ - private void buildReadOnly(final FormBuilder builder, final Label titleLabel) { - if (this.type == Type.MULTI || this.type == Type.MULTI_COMBO || this.type == Type.MULTI_CHECKBOX) { - final Composite composite = new Composite(builder.formParent, SWT.NONE); - final GridLayout gridLayout = new GridLayout(1, true); - //gridLayout.verticalSpacing = 5; - gridLayout.marginBottom = 5; - gridLayout.horizontalSpacing = 0; - gridLayout.marginLeft = 0; - gridLayout.marginHeight = 0; - gridLayout.marginWidth = 0; - composite.setLayout(gridLayout); - composite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false, this.spanInput, 1)); - if (StringUtils.isBlank(this.value)) { - final Label label = new Label(composite, SWT.NONE); - final GridData gridData = new GridData(SWT.FILL, SWT.TOP, true, true); - - label.setLayoutData(gridData); - label.setText(this.value); - } else { - final Collection keys = Arrays.asList(StringUtils.split(this.value, Constants.LIST_SEPARATOR)); - this.itemsSupplier.get() - .stream() - .filter(tuple -> keys.contains(tuple._1)) - .map(tuple -> tuple._1) - .forEach(v -> buildReadonlyLabel(composite, v, 1)); - } - } else { - builder.form.putReadonlyField( - this.name, - titleLabel, - buildReadonlyLabel(builder.formParent, this.value, this.spanInput)); - builder.setFieldVisible(this.visible, this.name); - } - } - - private Text buildReadonlyLabel(final Composite composite, final String valueKey, final int hspan) { - final Text label = new Text(composite, SWT.READ_ONLY); - final GridData gridData = new GridData(SWT.FILL, SWT.TOP, true, true, hspan, 1); - gridData.verticalIndent = 0; - gridData.horizontalIndent = 0; - label.setLayoutData(gridData); - - final Supplier valueSupplier = () -> this.itemsSupplier.get().stream() - .filter(tuple -> valueKey.equals(tuple._1)) - .findFirst() - .map(tuple -> tuple._2) - .orElse(Constants.EMPTY_NOTE); - final Consumer updateFunction = t -> t.setText(valueSupplier.get()); - - label.setText(valueSupplier.get()); - label.setData(PolyglotPageService.POLYGLOT_WIDGET_FUNCTION_KEY, updateFunction); - return label; - } - +/* + * 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.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import org.apache.commons.lang3.StringUtils; +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.Control; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Text; + +import ch.ethz.seb.sebserver.gbl.Constants; +import ch.ethz.seb.sebserver.gbl.util.Tuple; +import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; +import ch.ethz.seb.sebserver.gui.service.i18n.PolyglotPageService; +import ch.ethz.seb.sebserver.gui.widget.Selection; +import ch.ethz.seb.sebserver.gui.widget.Selection.Type; + +public final class SelectionFieldBuilder extends FieldBuilder { + + final Supplier>> itemsSupplier; + Consumer selectionListener = null; + final Selection.Type type; + + SelectionFieldBuilder( + final Selection.Type type, + final String name, + final LocTextKey label, + final String value, + final Supplier>> itemsSupplier) { + + super(name, label, value); + this.type = type; + this.itemsSupplier = itemsSupplier; + } + + public SelectionFieldBuilder withSelectionListener(final Consumer selectionListener) { + this.selectionListener = selectionListener; + return this; + } + + @Override + void build(final FormBuilder builder) { + final Control titleLabel = createTitleLabel(builder.formParent, builder, this); + + if (builder.readonly || this.readonly) { + buildReadOnly(builder, titleLabel); + } else { + buildInput(builder, titleLabel); + } + } + + private void buildInput(final FormBuilder builder, final Control titleLabel) { + + final Composite fieldGrid = createFieldGrid(builder.formParent, this.spanInput); + final String actionKey = (this.label != null) ? this.label.name + ".action" : null; + final Selection selection = builder.widgetFactory.selectionLocalized( + this.type, + fieldGrid, + this.itemsSupplier, + null, + null, + actionKey); + + final GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, false); + ((Control) selection).setLayoutData(gridData); + selection.select(this.value); + + final Label errorLabel = createErrorLabel(fieldGrid); + builder.form.putField(this.name, titleLabel, selection, errorLabel); + + if (this.selectionListener != null) { + ((Control) selection).addListener(SWT.Selection, e -> { + this.selectionListener.accept(builder.form); + }); + } + + builder.setFieldVisible(this.visible, this.name); + } + + /* Build the read-only representation of the selection field */ + private void buildReadOnly(final FormBuilder builder, final Control titleLabel) { + if (this.type == Type.MULTI || this.type == Type.MULTI_COMBO || this.type == Type.MULTI_CHECKBOX) { + final Composite composite = new Composite(builder.formParent, SWT.NONE); + final GridLayout gridLayout = new GridLayout(1, true); + //gridLayout.verticalSpacing = 5; + gridLayout.marginBottom = 5; + gridLayout.horizontalSpacing = 0; + gridLayout.marginLeft = 0; + gridLayout.marginHeight = 0; + gridLayout.marginWidth = 0; + composite.setLayout(gridLayout); + composite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false, this.spanInput, 1)); + if (StringUtils.isBlank(this.value)) { + final Label label = new Label(composite, SWT.NONE); + final GridData gridData = new GridData(SWT.FILL, SWT.TOP, true, true); + + label.setLayoutData(gridData); + label.setText(this.value); + } else { + final Collection keys = Arrays.asList(StringUtils.split(this.value, Constants.LIST_SEPARATOR)); + this.itemsSupplier.get() + .stream() + .filter(tuple -> keys.contains(tuple._1)) + .map(tuple -> tuple._1) + .forEach(v -> buildReadonlyLabel(composite, v, 1)); + } + } else { + builder.form.putReadonlyField( + this.name, + titleLabel, + buildReadonlyLabel(builder.formParent, this.value, this.spanInput)); + builder.setFieldVisible(this.visible, this.name); + } + } + + private Text buildReadonlyLabel(final Composite composite, final String valueKey, final int hspan) { + final Text label = new Text(composite, SWT.READ_ONLY); + final GridData gridData = new GridData(SWT.FILL, SWT.TOP, true, true, hspan, 1); + gridData.verticalIndent = 0; + gridData.horizontalIndent = 0; + label.setLayoutData(gridData); + + final Supplier valueSupplier = () -> this.itemsSupplier.get().stream() + .filter(tuple -> valueKey.equals(tuple._1)) + .findFirst() + .map(tuple -> tuple._2) + .orElse(Constants.EMPTY_NOTE); + final Consumer updateFunction = t -> t.setText(valueSupplier.get()); + + label.setText(valueSupplier.get()); + label.setData(PolyglotPageService.POLYGLOT_WIDGET_FUNCTION_KEY, updateFunction); + return label; + } + } \ No newline at end of file diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/form/TextFieldBuilder.java b/src/main/java/ch/ethz/seb/sebserver/gui/form/TextFieldBuilder.java index e6f4a2e0..8ca8d24c 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/form/TextFieldBuilder.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/form/TextFieldBuilder.java @@ -18,6 +18,7 @@ import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.RGB; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Text; @@ -88,7 +89,7 @@ public final class TextFieldBuilder extends FieldBuilder { @Override void build(final FormBuilder builder) { final boolean readonly = builder.readonly || this.readonly; - final Label titleLabel = createTitleLabel(builder.formParent, builder, this); + final Control titleLabel = createTitleLabel(builder.formParent, builder, this); final Composite fieldGrid = createFieldGrid(builder.formParent, this.spanInput); if (readonly && this.isHTML) { @@ -117,7 +118,7 @@ public final class TextFieldBuilder extends FieldBuilder { gridData.minimumHeight = this.areaMinHeight; } else if (this.isColorBox) { gridData.minimumHeight = WidgetFactory.TEXT_INPUT_MIN_HEIGHT; - textInput.setData(RWT.CUSTOM_VARIANT, "colorbox"); + textInput.setData(RWT.CUSTOM_VARIANT, WidgetFactory.CustomVariant.COLOR_BOX.key); } textInput.setLayoutData(gridData); if (StringUtils.isNoneBlank(this.value)) { diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/form/ThresholdListBuilder.java b/src/main/java/ch/ethz/seb/sebserver/gui/form/ThresholdListBuilder.java index ca4a8bb8..1483da3c 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/form/ThresholdListBuilder.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/form/ThresholdListBuilder.java @@ -1,89 +1,90 @@ -/* - * 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 java.util.stream.Collectors; - -import org.apache.commons.lang3.StringUtils; -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.gbl.Constants; -import ch.ethz.seb.sebserver.gbl.model.Domain; -import ch.ethz.seb.sebserver.gbl.model.exam.Indicator.IndicatorType; -import ch.ethz.seb.sebserver.gbl.model.exam.Indicator.Threshold; -import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; -import ch.ethz.seb.sebserver.gui.widget.ThresholdList; - -public class ThresholdListBuilder extends FieldBuilder> { - - private final Collection thresholds; - - protected ThresholdListBuilder( - final String name, - final LocTextKey label, - final Collection thresholds) { - - super(name, label, thresholds); - this.thresholds = thresholds; - } - - @Override - void build(final FormBuilder builder) { - final Label titleLabel = createTitleLabel(builder.formParent, builder, this); - if (builder.readonly || this.readonly) { - // No read-only view needed for this so far? - return; - } else { - - final Composite fieldGrid = createFieldGrid(builder.formParent, this.spanInput); - - final ThresholdList thresholdList = builder.widgetFactory.thresholdList( - fieldGrid, - fieldGrid.getParent().getParent(), - this.thresholds, - () -> { - try { - final String fieldValue = builder.form.getFieldValue(Domain.INDICATOR.ATTR_TYPE); - return IndicatorType.valueOf(fieldValue); - } catch (final Exception e) { - return null; - } - }); - - final GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, false); - thresholdList.setLayoutData(gridData); - - final Label errorLabel = createErrorLabel(fieldGrid); - builder.form.putField(this.name, titleLabel, thresholdList, errorLabel); - builder.setFieldVisible(this.visible, this.name); - } - - } - - public static final String thresholdsToFormURLEncodedStringValue(final Collection thresholds) { - if (thresholds == null || thresholds.isEmpty()) { - return null; - } - - // thresholds={value}|{color},thresholds={value}|{color}... - return StringUtils.join(thresholds.stream() - .map(t -> Domain.THRESHOLD.REFERENCE_NAME - + Constants.FORM_URL_ENCODED_NAME_VALUE_SEPARATOR - + String.valueOf(t.getValue()) - + Constants.EMBEDDED_LIST_SEPARATOR - + t.getColor()) - .collect(Collectors.toList()), - Constants.LIST_SEPARATOR); - } - -} +/* + * 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 java.util.stream.Collectors; + +import org.apache.commons.lang3.StringUtils; +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; + +import ch.ethz.seb.sebserver.gbl.Constants; +import ch.ethz.seb.sebserver.gbl.model.Domain; +import ch.ethz.seb.sebserver.gbl.model.exam.Indicator.IndicatorType; +import ch.ethz.seb.sebserver.gbl.model.exam.Indicator.Threshold; +import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; +import ch.ethz.seb.sebserver.gui.widget.ThresholdList; + +public class ThresholdListBuilder extends FieldBuilder> { + + private final Collection thresholds; + + protected ThresholdListBuilder( + final String name, + final LocTextKey label, + final Collection thresholds) { + + super(name, label, thresholds); + this.thresholds = thresholds; + } + + @Override + void build(final FormBuilder builder) { + final Control titleLabel = createTitleLabel(builder.formParent, builder, this); + if (builder.readonly || this.readonly) { + // No read-only view needed for this so far? + return; + } else { + + final Composite fieldGrid = createFieldGrid(builder.formParent, this.spanInput); + + final ThresholdList thresholdList = builder.widgetFactory.thresholdList( + fieldGrid, + fieldGrid.getParent().getParent(), + this.thresholds, + () -> { + try { + final String fieldValue = builder.form.getFieldValue(Domain.INDICATOR.ATTR_TYPE); + return IndicatorType.valueOf(fieldValue); + } catch (final Exception e) { + return null; + } + }); + + final GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, false); + thresholdList.setLayoutData(gridData); + + final Label errorLabel = createErrorLabel(fieldGrid); + builder.form.putField(this.name, titleLabel, thresholdList, errorLabel); + builder.setFieldVisible(this.visible, this.name); + } + + } + + public static final String thresholdsToFormURLEncodedStringValue(final Collection thresholds) { + if (thresholds == null || thresholds.isEmpty()) { + return null; + } + + // thresholds={value}|{color},thresholds={value}|{color}... + return StringUtils.join(thresholds.stream() + .map(t -> Domain.THRESHOLD.REFERENCE_NAME + + Constants.FORM_URL_ENCODED_NAME_VALUE_SEPARATOR + + String.valueOf(t.getValue()) + + Constants.EMBEDDED_LIST_SEPARATOR + + t.getColor()) + .collect(Collectors.toList()), + Constants.LIST_SEPARATOR); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/ResourceService.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/ResourceService.java index af1f978d..49b3379d 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/ResourceService.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/ResourceService.java @@ -20,6 +20,7 @@ import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; +import ch.ethz.seb.sebserver.gbl.model.sebconfig.SebClientConfig; import ch.ethz.seb.sebserver.gbl.util.Tuple3; import ch.ethz.seb.sebserver.gbl.util.Utils; import org.apache.commons.lang3.StringUtils; @@ -107,6 +108,7 @@ public class ResourceService { public static final String CONFIG_ATTRIBUTE_TYPE_PREFIX = "sebserver.configtemplate.attr.type."; public static final String SEB_RESTRICTION_WHITE_LIST_PREFIX = "sebserver.exam.form.sebrestriction.whiteListPaths."; public static final String SEB_RESTRICTION_PERMISSIONS_PREFIX = "sebserver.exam.form.sebrestriction.permissions."; + public static final String SEB_CLIENT_CONFIG_PURPOSE_PREFIX = "sebserver.clientconfig.config.purpose."; public static final EnumSet ATTRIBUTE_TYPES_NOT_DISPLAYED = EnumSet.of( AttributeType.LABEL, @@ -656,4 +658,16 @@ public class ResourceService { .call(); } + public List> sebClientConfigPurposeResources() { + return Arrays.stream(SebClientConfig.ConfigPurpose.values()) + .map(type -> new Tuple3<>( + type.name(), + this.i18nSupport.getText(SEB_CLIENT_CONFIG_PURPOSE_PREFIX + type.name()), + Utils.formatLineBreaks(this.i18nSupport.getText( + SEB_CLIENT_CONFIG_PURPOSE_PREFIX + type.name() + Constants.TOOLTIP_TEXT_KEY_SUFFIX, + StringUtils.EMPTY)))) + .sorted(RESOURCE_COMPARATOR) + .collect(Collectors.toList()); + } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/examconfig/impl/PassworFieldBuilder.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/examconfig/impl/PasswordFieldBuilder.java similarity index 70% rename from src/main/java/ch/ethz/seb/sebserver/gui/service/examconfig/impl/PassworFieldBuilder.java rename to src/main/java/ch/ethz/seb/sebserver/gui/service/examconfig/impl/PasswordFieldBuilder.java index bb3b12fc..e61cd920 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/examconfig/impl/PassworFieldBuilder.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/examconfig/impl/PasswordFieldBuilder.java @@ -1,178 +1,181 @@ -/* - * 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.examconfig.impl; - -import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; - -import org.apache.commons.codec.binary.Hex; -import org.eclipse.rap.rwt.RWT; -import org.eclipse.swt.SWT; -import org.eclipse.swt.layout.GridData; -import org.eclipse.swt.widgets.Composite; -import org.eclipse.swt.widgets.Label; -import org.eclipse.swt.widgets.Listener; -import org.eclipse.swt.widgets.Text; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.context.annotation.Lazy; -import org.springframework.stereotype.Component; - -import ch.ethz.seb.sebserver.gbl.model.sebconfig.AttributeType; -import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationAttribute; -import ch.ethz.seb.sebserver.gbl.model.sebconfig.Orientation; -import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; -import ch.ethz.seb.sebserver.gui.form.FieldBuilder; -import ch.ethz.seb.sebserver.gui.service.examconfig.InputField; -import ch.ethz.seb.sebserver.gui.service.examconfig.InputFieldBuilder; -import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; -import ch.ethz.seb.sebserver.gui.widget.WidgetFactory; -import ch.ethz.seb.sebserver.gui.widget.WidgetFactory.CustomVariant; - -@Lazy -@Component -@GuiProfile -public class PassworFieldBuilder implements InputFieldBuilder { - - private static final Logger log = LoggerFactory.getLogger(PassworFieldBuilder.class); - - private static final LocTextKey VAL_CONFIRM_PWD_TEXT_KEY = - new LocTextKey("sebserver.examconfig.props.validation.password.confirm"); - - @Override - public boolean builderFor( - final ConfigurationAttribute attribute, - final Orientation orientation) { - - if (attribute == null) { - return false; - } - - return AttributeType.PASSWORD_FIELD == attribute.type; - } - - @Override - public InputField createInputField( - final Composite parent, - final ConfigurationAttribute attribute, - final ViewContext viewContext) { - - final Orientation orientation = viewContext - .getOrientation(attribute.id); - final Composite innerGrid = InputFieldBuilder - .createInnerGrid(parent, attribute, orientation); - - final Text passwordInput = new Text(innerGrid, SWT.LEFT | SWT.BORDER | SWT.PASSWORD); - final GridData passwordInputLD = new GridData(SWT.FILL, SWT.FILL, true, false); - passwordInput.setLayoutData(passwordInputLD); - final Text confirmInput = new Text(innerGrid, SWT.LEFT | SWT.BORDER | SWT.PASSWORD); - final GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, false); - gridData.verticalIndent = 14; - confirmInput.setLayoutData(gridData); - - final PasswordInputField passwordInputField = new PasswordInputField( - attribute, - orientation, - passwordInput, - confirmInput, - FieldBuilder.createErrorLabel(innerGrid)); - - if (viewContext.readonly) { - passwordInput.setEditable(false); - passwordInput.setData(RWT.CUSTOM_VARIANT, CustomVariant.CONFIG_INPUT_READONLY.key); - passwordInputLD.heightHint = WidgetFactory.TEXT_INPUT_MIN_HEIGHT; - confirmInput.setEditable(false); - confirmInput.setData(RWT.CUSTOM_VARIANT, CustomVariant.CONFIG_INPUT_READONLY.key); - gridData.heightHint = WidgetFactory.TEXT_INPUT_MIN_HEIGHT; - } else { - final Listener valueChangeEventListener = event -> { - passwordInputField.clearError(); - - final String pwd = passwordInput.getText(); - final String confirm = confirmInput.getText(); - - if (passwordInputField.initValue != null && passwordInputField.initValue.equals(pwd)) { - return; - } - - if (!pwd.equals(confirm)) { - passwordInputField.showError(viewContext - .getI18nSupport() - .getText(VAL_CONFIRM_PWD_TEXT_KEY)); - return; - } - - final String hashedPWD = passwordInputField.getValue(); - if (hashedPWD != null) { - viewContext.getValueChangeListener().valueChanged( - viewContext, - attribute, - hashedPWD, - passwordInputField.listIndex); - } - }; - - passwordInput.addListener(SWT.FocusOut, valueChangeEventListener); - passwordInput.addListener(SWT.Traverse, valueChangeEventListener); - confirmInput.addListener(SWT.FocusOut, valueChangeEventListener); - confirmInput.addListener(SWT.Traverse, valueChangeEventListener); - } - return passwordInputField; - } - - static final class PasswordInputField extends AbstractInputField { - - private final Text confirm; - - PasswordInputField( - final ConfigurationAttribute attribute, - final Orientation orientation, - final Text control, - final Text confirm, - final Label errorLabel) { - - super(attribute, orientation, control, errorLabel); - this.confirm = confirm; - } - - @Override - protected void setValueToControl(final String value) { - // TODO clarify setting some "fake" input when a password is set (like in config tool) - if (value != null) { - this.control.setText(value); - this.confirm.setText(value); - } - } - - @Override - public String getValue() { - String hashedPWD; - try { - hashedPWD = hashPassword(this.control.getText()); - } catch (final NoSuchAlgorithmException e) { - log.error("Failed to hash password: ", e); - showError("Failed to hash password"); - hashedPWD = null; - } - - return hashedPWD; - } - - private String hashPassword(final String pwd) throws NoSuchAlgorithmException { - final MessageDigest digest = MessageDigest.getInstance("SHA-256"); - final byte[] encodedhash = digest.digest( - pwd.getBytes(StandardCharsets.UTF_8)); - - return Hex.encodeHexString(encodedhash); - } - - } - -} +/* + * 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.examconfig.impl; + +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.Orientation; +import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; +import ch.ethz.seb.sebserver.gbl.util.Cryptor; +import ch.ethz.seb.sebserver.gui.form.FieldBuilder; +import ch.ethz.seb.sebserver.gui.service.examconfig.InputField; +import ch.ethz.seb.sebserver.gui.service.examconfig.InputFieldBuilder; +import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; +import ch.ethz.seb.sebserver.gui.widget.PasswordInput; +import ch.ethz.seb.sebserver.gui.widget.WidgetFactory; +import ch.ethz.seb.sebserver.gui.widget.WidgetFactory.CustomVariant; +import org.apache.commons.lang3.StringUtils; +import org.eclipse.rap.rwt.RWT; +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Listener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +@Lazy +@Component +@GuiProfile +public class PasswordFieldBuilder implements InputFieldBuilder { + + private static final Logger log = LoggerFactory.getLogger(PasswordFieldBuilder.class); + + private static final LocTextKey VAL_CONFIRM_PWD_TEXT_KEY = + new LocTextKey("sebserver.examconfig.props.validation.password.confirm"); + + private final Cryptor cryptor; + private final WidgetFactory widgetFactory; + + public PasswordFieldBuilder( + final WidgetFactory widgetFactory, + final Cryptor cryptor) { + + this.cryptor = cryptor; + this.widgetFactory = widgetFactory; + } + + @Override + public boolean builderFor( + final ConfigurationAttribute attribute, + final Orientation orientation) { + + if (attribute == null) { + return false; + } + + return AttributeType.PASSWORD_FIELD == attribute.type; + } + + @Override + public InputField createInputField( + final Composite parent, + final ConfigurationAttribute attribute, + final ViewContext viewContext) { + + final Orientation orientation = viewContext + .getOrientation(attribute.id); + final Composite innerGrid = InputFieldBuilder + .createInnerGrid(parent, attribute, orientation); + + final PasswordInput passwordInput = new PasswordInput(innerGrid, widgetFactory); + final GridData passwordInputLD = new GridData(SWT.FILL, SWT.FILL, true, true); + passwordInput.setLayoutData(passwordInputLD); + + final PasswordInput confirmInput = new PasswordInput(innerGrid, widgetFactory); + final GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, true); + gridData.verticalIndent = 14; + confirmInput.setLayoutData(gridData); + innerGrid.setData("isPlainText", false); + + final PasswordInputField passwordInputField = new PasswordInputField( + attribute, + orientation, + passwordInput, + confirmInput, + FieldBuilder.createErrorLabel(innerGrid), + cryptor); + + if (viewContext.readonly) { + passwordInput.setEditable(false); + passwordInputLD.heightHint = WidgetFactory.TEXT_INPUT_MIN_HEIGHT; + confirmInput.setEditable(false); + confirmInput.setData(RWT.CUSTOM_VARIANT, CustomVariant.CONFIG_INPUT_READONLY.key); + gridData.heightHint = WidgetFactory.TEXT_INPUT_MIN_HEIGHT; + } else { + final Listener valueChangeEventListener = event -> { + passwordInputField.clearError(); + + final CharSequence pwd = passwordInput.getValue(); + final CharSequence confirm = confirmInput.getValue(); + + if (passwordInputField.initValue != null && passwordInputField.initValue.equals(pwd)) { + return; + } + + if (!pwd.equals(confirm)) { + passwordInputField.showError(viewContext + .getI18nSupport() + .getText(VAL_CONFIRM_PWD_TEXT_KEY)); + return; + } + + final String hashedPWD = passwordInputField.getValue(); + if (hashedPWD != null) { + viewContext.getValueChangeListener().valueChanged( + viewContext, + attribute, + hashedPWD, + passwordInputField.listIndex); + } + }; + + passwordInput.addListener(SWT.FocusOut, valueChangeEventListener); + passwordInput.addListener(SWT.Traverse, valueChangeEventListener); + confirmInput.addListener(SWT.FocusOut, valueChangeEventListener); + confirmInput.addListener(SWT.Traverse, valueChangeEventListener); + } + return passwordInputField; + } + + static final class PasswordInputField extends AbstractInputField { + + private final PasswordInput confirm; + private final Cryptor cryptor; + + PasswordInputField( + final ConfigurationAttribute attribute, + final Orientation orientation, + final PasswordInput control, + final PasswordInput confirm, + final Label errorLabel, + final Cryptor cryptor) { + + super(attribute, orientation, control, errorLabel); + this.confirm = confirm; + this.cryptor = cryptor; + } + + @Override + protected void setValueToControl(final String value) { + if (StringUtils.isNotBlank(value)) { + CharSequence pwd = cryptor.decrypt(value); + this.control.setValue(pwd.toString()); + this.confirm.setValue(pwd.toString()); + } else { + this.control.setValue(StringUtils.EMPTY); + this.confirm.setValue(StringUtils.EMPTY); + } + } + + @Override + public String getValue() { + final CharSequence pwd = this.control.getValue(); + if (StringUtils.isNotBlank(pwd)) { + return cryptor.encrypt(pwd).toString(); + } + + return StringUtils.EMPTY; + } + + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/PageServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/PageServiceImpl.java index dfc95b02..570f90a9 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/PageServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/PageServiceImpl.java @@ -20,6 +20,7 @@ import java.util.function.Supplier; import javax.servlet.http.HttpSession; +import ch.ethz.seb.sebserver.gbl.util.Cryptor; import org.eclipse.rap.rwt.RWT; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -78,6 +79,7 @@ public class PageServiceImpl implements PageService { private static final String ATTR_PAGE_STATE = "PAGE_STATE"; private static final ListenerComparator LIST_COMPARATOR = new ListenerComparator(); + private final Cryptor cryptor; private final JSONMapper jsonMapper; private final WidgetFactory widgetFactory; private final PolyglotPageService polyglotPageService; @@ -85,12 +87,14 @@ public class PageServiceImpl implements PageService { private final CurrentUser currentUser; public PageServiceImpl( + final Cryptor cryptor, final JSONMapper jsonMapper, final WidgetFactory widgetFactory, final PolyglotPageService polyglotPageService, final ResourceService resourceService, final CurrentUser currentUser) { + this.cryptor = cryptor; this.jsonMapper = jsonMapper; this.widgetFactory = widgetFactory; this.polyglotPageService = polyglotPageService; @@ -337,7 +341,7 @@ public class PageServiceImpl implements PageService { @Override public FormBuilder formBuilder(final PageContext pageContext, final int rows) { - return new FormBuilder(this, pageContext, rows); + return new FormBuilder(this, pageContext, cryptor, rows); } @Override diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/widget/PasswordInput.java b/src/main/java/ch/ethz/seb/sebserver/gui/widget/PasswordInput.java new file mode 100644 index 00000000..7b0a6df9 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/widget/PasswordInput.java @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2020 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 ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; +import ch.ethz.seb.sebserver.gui.service.page.PageService; +import org.apache.commons.lang3.StringUtils; +import org.eclipse.rap.rwt.RWT; +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 org.eclipse.swt.widgets.Text; + +public class PasswordInput extends Composite { + + public static final LocTextKey PLAIN_TEXT_VIEW_TOOLTIP_KEY = + new LocTextKey("sebserver.overall.action.showPassword.tooltip"); + + + private final WidgetFactory widgetFactory; + private final Composite inputAnchor; + private final Label visibilityButton; + + private Text passwordInput = null; + private boolean isPlainText = true; + private boolean isEditable = true; + + public PasswordInput(final Composite parent, final WidgetFactory widgetFactory) { + super(parent, SWT.NONE); + this.widgetFactory = widgetFactory; + + GridLayout gridLayout = new GridLayout(2, false); + gridLayout.horizontalSpacing = 0; + gridLayout.verticalSpacing = 0; + gridLayout.marginHeight = 0; + gridLayout.marginWidth = 0; + this.setLayout(gridLayout); + this.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false)); + + inputAnchor = new Composite(this, SWT.NONE); + gridLayout = new GridLayout(1, false); + gridLayout.horizontalSpacing = 0; + gridLayout.verticalSpacing = 0; + gridLayout.marginHeight = 0; + gridLayout.marginWidth = 0; + inputAnchor.setLayout(gridLayout); + inputAnchor.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false)); + + + + visibilityButton = widgetFactory.imageButton( + WidgetFactory.ImageIcon.VISIBILITY, + this, + PLAIN_TEXT_VIEW_TOOLTIP_KEY, + event -> changePasswordView()); + GridData ld = new GridData(SWT.RIGHT, SWT.BOTTOM, false, false); + ld.heightHint = 22; + ld.horizontalIndent = 5; + visibilityButton.setLayoutData(ld); + + changePasswordView(); + + } + + private void changePasswordView() { + final String value = (this.passwordInput != null) ? this.passwordInput.getText() : null; + final boolean buildPassword = this.isPlainText; + + if (this.passwordInput != null) { + PageService.clearComposite(this.inputAnchor); + } + + Text passwordInput = new Text( + inputAnchor, + SWT.LEFT | SWT.BORDER | (buildPassword ? SWT.PASSWORD : SWT.NONE)); + GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, true); + passwordInput.setLayoutData(gridData); + passwordInput.setText(value != null ? value : StringUtils.EMPTY); + if (!buildPassword) { + passwordInput.setEditable(false); + } else { + passwordInput.setEditable(isEditable); + passwordInput.setData(RWT.CUSTOM_VARIANT, WidgetFactory.CustomVariant.CONFIG_INPUT_READONLY.key); + if (!isEditable) { + gridData.heightHint = 21; + } + } + + if (buildPassword) { + passwordInput.addListener(SWT.FocusOut, event -> super.notifyListeners(SWT.FocusOut, event)); + passwordInput.addListener(SWT.Traverse, event -> super.notifyListeners(SWT.Traverse, event)); + this.visibilityButton.setImage(WidgetFactory.ImageIcon.VISIBILITY.getImage(getDisplay())); + } else { + passwordInput.setData(RWT.CUSTOM_VARIANT, WidgetFactory.CustomVariant.PLAIN_PWD.key); + this.visibilityButton.setImage(WidgetFactory.ImageIcon.VISIBILITY_OFF.getImage(getDisplay())); + } + + this.passwordInput = passwordInput; + this.isPlainText = !this.isPlainText; + + super.layout(true, true); + } + + public void setValue(CharSequence value) { + if (passwordInput != null) { + passwordInput.setText(value != null ? value.toString() : StringUtils.EMPTY); + } + } + + public CharSequence getValue() { + if (passwordInput != null) { + return passwordInput.getText(); + } + + return null; + } + + + public void setEditable(boolean editable) { + this.isEditable = editable; + this.isPlainText = !this.isPlainText; + this.changePasswordView(); + } +} 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 3f78c8f6..4675e3c0 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 @@ -1,764 +1,768 @@ -/* - * Copyright (c) 2018 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 static ch.ethz.seb.sebserver.gui.service.i18n.PolyglotPageService.POLYGLOT_WIDGET_FUNCTION_KEY; - -import java.io.InputStream; -import java.util.Collection; -import java.util.List; -import java.util.Locale; -import java.util.function.Consumer; -import java.util.function.Supplier; - -import org.eclipse.rap.rwt.RWT; -import org.eclipse.rap.rwt.client.service.JavaScriptExecutor; -import org.eclipse.rap.rwt.widgets.WidgetUtil; -import org.eclipse.swt.SWT; -import org.eclipse.swt.graphics.Device; -import org.eclipse.swt.graphics.Image; -import org.eclipse.swt.graphics.ImageData; -import org.eclipse.swt.layout.GridData; -import org.eclipse.swt.layout.GridLayout; -import org.eclipse.swt.widgets.Button; -import org.eclipse.swt.widgets.ColorDialog; -import org.eclipse.swt.widgets.Composite; -import org.eclipse.swt.widgets.DateTime; -import org.eclipse.swt.widgets.Group; -import org.eclipse.swt.widgets.Label; -import org.eclipse.swt.widgets.Listener; -import org.eclipse.swt.widgets.TabFolder; -import org.eclipse.swt.widgets.TabItem; -import org.eclipse.swt.widgets.Table; -import org.eclipse.swt.widgets.TableColumn; -import org.eclipse.swt.widgets.Text; -import org.eclipse.swt.widgets.Tree; -import org.eclipse.swt.widgets.TreeItem; -import org.eclipse.swt.widgets.Widget; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.context.annotation.Lazy; -import org.springframework.stereotype.Service; - -import ch.ethz.seb.sebserver.gbl.model.exam.Indicator.IndicatorType; -import ch.ethz.seb.sebserver.gbl.model.exam.Indicator.Threshold; -import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; -import ch.ethz.seb.sebserver.gbl.util.Tuple; -import ch.ethz.seb.sebserver.gbl.util.Utils; -import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition; -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.i18n.PolyglotPageService; -import ch.ethz.seb.sebserver.gui.service.page.PageService; -import ch.ethz.seb.sebserver.gui.service.page.impl.DefaultPageLayout; -import ch.ethz.seb.sebserver.gui.service.push.ServerPushService; - -@Lazy -@Service -@GuiProfile -public class WidgetFactory { - - private static final String ADD_HTML_ATTR_ARIA_ROLE = "role"; - private static final String ADD_HTML_ATTR_TEST_ID = "test-id"; - - private static final Logger log = LoggerFactory.getLogger(WidgetFactory.class); - - public static final int TEXT_AREA_INPUT_MIN_HEIGHT = 100; - public static final int TEXT_INPUT_MIN_HEIGHT = 24; - - public enum ImageIcon { - MAXIMIZE("maximize.png"), - MINIMIZE("minimize.png"), - MANDATORY("mandatory.png"), - ADD("add.png"), - REMOVE("remove.png"), - ADD_BOX("add_box.png"), - ADD_BOX_WHITE("add_box_w.png"), - REMOVE_BOX("remove_box.png"), - REMOVE_BOX_WHITE("remove_box_w.png"), - EDIT("edit.png"), - EDIT_SETTINGS("settings.png"), - TEST("test.png"), - COPY("copy.png"), - IMPORT("import.png"), - CANCEL("cancel.png"), - CANCEL_EDIT("cancelEdit.png"), - SHOW("show.png"), - ACTIVE("active.png"), - INACTIVE("inactive.png"), - TOGGLE_ON("toggle_on.png"), - TOGGLE_OFF("toggle_off.png"), - SWITCH("switch.png"), - YES("yes.png"), - NO("no.png"), - SAVE("save.png"), - EXPORT("export.png"), - SECURE("secure.png"), - NEW("new.png"), - DELETE("delete.png"), - SEARCH("lens.png"), - UNDO("undo.png"), - COLOR("color.png"), - USER("user.png"), - INSTITUTION("institution.png"), - LMS_SETUP("lmssetup.png"), - INDICATOR("indicator.png"), - TEMPLATE("template.png"), - DISABLE("disable.png"), - SEND_QUIT("send-quit.png"), - HELP("help.png"), - LOCK("lock.png"), - UNLOCK("unlock.png"), - RESTRICTION("restriction.png"); - - public String fileName; - private ImageData image = null; - private ImageData greyedImage = null; - - private ImageIcon(final String fileName) { - this.fileName = fileName; - } - - public Image getImage(final Device device) { - if (this.image == null) { - try { - final InputStream resourceAsStream = - WidgetFactory.class.getResourceAsStream("/static/images/" + this.fileName); - this.image = new ImageData(resourceAsStream); - } catch (final Exception e) { - log.error("Failed to load resource image: {}", this.fileName, e); - } - } - - return new Image(device, this.image); - } - - public Image getGreyedImage(final Device device) { - if (this.greyedImage == null) { - try { - final InputStream resourceAsStream = - WidgetFactory.class.getResourceAsStream("/static/images/" + this.fileName); - this.greyedImage = new ImageData(resourceAsStream); - this.greyedImage.alpha = -1; - for (int y = 0; y < this.greyedImage.height; y++) { - for (int x = 0; x < this.greyedImage.width; x++) { - this.greyedImage.setAlpha(x, y, this.greyedImage.getAlpha(x, y) / 3); - } - } - } catch (final Exception e) { - log.error("Failed to load resource image: {}", this.fileName, e); - } - } - - return new Image(device, this.greyedImage); - } - } - - public enum CustomVariant { - TEXT_H1("h1"), - TEXT_H2("h2"), - TEXT_H3("h3"), - IMAGE_BUTTON("imageButton"), - TEXT_ACTION("action"), - TEXT_READONLY("readonlyText"), - - FORM_CENTER("form-center"), - SELECTION("selection"), - SELECTED("selected"), - - ACTIVITY_TREE_SECTION("treesection"), - - FOOTER("footer"), - TITLE_LABEL("head"), - - MESSAGE("message"), - ERROR("error"), - WARNING("warning"), - CONFIG_INPUT_READONLY("inputreadonly"), - - DARK_COLOR_LABEL("colordark"), - LIGHT_COLOR_LABEL("colorlight"), - - LOGIN("login"), - LOGIN_BACK("login-back"), - SCROLL("scroll"), - - LIST_NAVIGATION("list-nav") - - ; - - public final String key; - - private CustomVariant(final String key) { - this.key = key; - } - } - - private final PolyglotPageService polyglotPageService; - private final I18nSupport i18nSupport; - private final ServerPushService serverPushService; - - public WidgetFactory( - final PolyglotPageService polyglotPageService, - final ServerPushService serverPushService) { - - this.polyglotPageService = polyglotPageService; - this.i18nSupport = polyglotPageService.getI18nSupport(); - this.serverPushService = serverPushService; - } - - public I18nSupport getI18nSupport() { - return this.i18nSupport; - } - - public Composite defaultPageLayout(final Composite parent) { - final Composite content = new Composite(parent, SWT.NONE); - final GridLayout contentLayout = new GridLayout(); - contentLayout.marginLeft = 10; - content.setLayout(contentLayout); - final GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, true); - content.setLayoutData(gridData); - return content; - } - - public Composite defaultPageLayout(final Composite parent, final LocTextKey title) { - final Composite defaultPageLayout = defaultPageLayout(parent); - final Label labelLocalizedTitle = labelLocalizedTitle(defaultPageLayout, title); - final GridData gridData = new GridData(SWT.FILL, SWT.TOP, true, false); - labelLocalizedTitle.setLayoutData(gridData); - return defaultPageLayout; - } - - public Composite defaultPageLayout( - final Composite parent, - final LocTextKey title, - final ActionDefinition actionDefinition) { - - final Composite defaultPageLayout = defaultPageLayout(parent); - final Label labelLocalizedTitle = labelLocalizedTitle(defaultPageLayout, title); - labelLocalizedTitle.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false)); - return defaultPageLayout; - } - - public Composite formGrid(final Composite parent, final int rows) { - final Composite grid = new Composite(parent, SWT.NONE); - final GridLayout layout = new GridLayout(rows, true); - layout.horizontalSpacing = 10; - layout.verticalSpacing = 10; - layout.marginBottom = 10; - layout.marginLeft = 10; - layout.marginTop = 0; - grid.setLayout(layout); - grid.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false)); - return grid; - } - - /** Use this to create a scrolled Composite for usual popup forms - * - * @param parent The parent Composite - * @return the scrolled Composite to add the form content */ - public Composite createPopupScrollComposite(final Composite parent) { - final Composite grid = PageService.createManagedVScrolledComposite( - parent, - scrolledComposite -> { - final Composite g = new Composite(scrolledComposite, SWT.NONE); - g.setLayout(new GridLayout()); - g.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false)); - return g; - }, - false); - return grid; - } - - public Composite createWarningPanel(final Composite parent) { - final Composite composite = new Composite(parent, SWT.NONE); - composite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false)); - final GridLayout gridLayout = new GridLayout(1, true); - gridLayout.marginWidth = 20; - gridLayout.marginHeight = 20; - composite.setLayout(gridLayout); - composite.setData(RWT.CUSTOM_VARIANT, CustomVariant.WARNING.key); - return composite; - } - - public Button buttonLocalized(final Composite parent, final String locTextKey) { - final Button button = new Button(parent, SWT.NONE); - this.polyglotPageService.injectI18n(button, new LocTextKey(locTextKey)); - return button; - } - - public Button buttonLocalized(final Composite parent, final LocTextKey locTextKey) { - final Button button = new Button(parent, SWT.NONE); - this.polyglotPageService.injectI18n(button, locTextKey); - return button; - } - - public Button buttonLocalized(final Composite parent, final CustomVariant variant, final String locTextKey) { - final Button button = new Button(parent, SWT.NONE); - this.polyglotPageService.injectI18n(button, new LocTextKey(locTextKey)); - button.setData(RWT.CUSTOM_VARIANT, variant.key); - return button; - } - - public Button buttonLocalized( - final Composite parent, - final int type, - final LocTextKey locTextKey, - final LocTextKey toolTipKey) { - - final Button button = new Button(parent, type); - this.polyglotPageService.injectI18n(button, locTextKey, toolTipKey); - return button; - } - - public Label label(final Composite parent, final String text) { - final Label label = new Label(parent, SWT.NONE); - label.setText(text); - return label; - } - - public Label labelLocalized(final Composite parent, final String locTextKey) { - final Label label = new Label(parent, SWT.NONE); - this.polyglotPageService.injectI18n(label, new LocTextKey(locTextKey)); - return label; - } - - public Label labelLocalized(final Composite parent, final LocTextKey locTextKey) { - final Label label = new Label(parent, SWT.NONE); - this.polyglotPageService.injectI18n(label, locTextKey); - return label; - } - - public Label labelLocalized(final Composite parent, final CustomVariant variant, final LocTextKey locTextKey) { - final Label label = new Label(parent, SWT.NONE); - this.polyglotPageService.injectI18n(label, locTextKey); - label.setData(RWT.CUSTOM_VARIANT, variant.key); - label.setData(RWT.MARKUP_ENABLED, true); - return label; - } - - public Label labelLocalized( - final Composite parent, - final LocTextKey locTextKey, - final LocTextKey locToolTextKey) { - - final Label label = new Label(parent, SWT.NONE); - this.polyglotPageService.injectI18n(label, locTextKey, locToolTextKey); - return label; - } - - public Label labelLocalized( - final Composite parent, - final CustomVariant variant, - final LocTextKey locTextKey, - final LocTextKey locToolTextKey) { - - final Label label = new Label(parent, SWT.NONE); - this.polyglotPageService.injectI18n(label, locTextKey, locToolTextKey); - label.setData(RWT.CUSTOM_VARIANT, variant.key); - return label; - } - - public Label labelLocalizedTitle(final Composite content, final LocTextKey locTextKey) { - final Label labelLocalized = labelLocalized(content, CustomVariant.TEXT_H1, locTextKey); - labelLocalized.setLayoutData(new GridData(SWT.TOP, SWT.LEFT, true, false)); - return labelLocalized; - } - - public Text textInput(final Composite content) { - return textInput(content, false, false); - } - - public Text textLabel(final Composite content) { - return textInput(content, false, true); - } - - public Text passwordInput(final Composite content) { - return textInput(content, true, false); - } - - public Text textAreaInput(final Composite content, final boolean readonly) { - return readonly - ? new Text(content, SWT.LEFT | SWT.MULTI) - : new Text(content, SWT.LEFT | SWT.BORDER | SWT.MULTI); - } - - public Text textInput(final Composite content, final boolean password, final boolean readonly) { - return readonly - ? new Text(content, SWT.LEFT) - : new Text(content, (password) - ? SWT.LEFT | SWT.BORDER | SWT.PASSWORD - : SWT.LEFT | SWT.BORDER); - } - - public Text numberInput(final Composite content, final Consumer numberCheck) { - return numberInput(content, numberCheck, false); - } - - public Text numberInput(final Composite content, final Consumer numberCheck, final boolean readonly) { - if (readonly) { - return new Text(content, SWT.RIGHT | SWT.READ_ONLY); - } - - final Text numberInput = new Text(content, SWT.RIGHT | SWT.BORDER); - if (numberCheck != null) { - numberInput.addListener(SWT.Verify, event -> { - final String value = event.text; - try { - numberCheck.accept(value); - } catch (final Exception e) { - event.doit = false; - } - }); - } - return numberInput; - } - - public Group groupLocalized( - final Composite parent, - final int columns, - final LocTextKey locTextKey) { - - return groupLocalized(parent, columns, locTextKey, null); - } - - public Group groupLocalized( - final Composite parent, - final int columns, - final LocTextKey locTextKey, - final LocTextKey locTooltipKey) { - - final Group group = new Group(parent, SWT.NONE); - final GridLayout gridLayout = new GridLayout(columns, true); - gridLayout.verticalSpacing = 0; - gridLayout.horizontalSpacing = 0; - gridLayout.marginHeight = 0; - group.setLayout(gridLayout); - - this.polyglotPageService.injectI18n(group, locTextKey, locTooltipKey); - return group; - } - - public Tree treeLocalized(final Composite parent, final int style) { - final Tree tree = new Tree(parent, style); - this.polyglotPageService.injectI18n(tree); - return tree; - } - - public TreeItem treeItemLocalized(final Tree parent, final String locTextKey) { - final TreeItem item = new TreeItem(parent, SWT.NONE); - this.polyglotPageService.injectI18n(item, new LocTextKey(locTextKey)); - return item; - } - - public TreeItem treeItemLocalized(final Tree parent, final LocTextKey locTextKey) { - final TreeItem item = new TreeItem(parent, SWT.NONE); - this.polyglotPageService.injectI18n(item, locTextKey); - return item; - } - - public TreeItem treeItemLocalized(final TreeItem parent, final String locTextKey) { - final TreeItem item = new TreeItem(parent, SWT.NONE); - this.polyglotPageService.injectI18n(item, new LocTextKey(locTextKey)); - return item; - } - - public TreeItem treeItemLocalized(final TreeItem parent, final LocTextKey locTextKey) { - final TreeItem item = new TreeItem(parent, SWT.NONE); - this.polyglotPageService.injectI18n(item, locTextKey); - return item; - } - - public Table tableLocalized(final Composite parent) { - final Table table = new Table(parent, SWT.NO_SCROLL); - this.polyglotPageService.injectI18n(table); - return table; - } - - public Table tableLocalized(final Composite parent, final int style) { - final Table table = new Table(parent, style); - this.polyglotPageService.injectI18n(table); - return table; - } - - public TableColumn tableColumnLocalized( - final Table table, - final LocTextKey locTextKey) { - - return tableColumnLocalized(table, locTextKey, null); - } - - public TableColumn tableColumnLocalized( - final Table table, - final LocTextKey locTextKey, - final LocTextKey toolTipKey) { - - final TableColumn tableColumn = new TableColumn(table, SWT.NONE); - this.polyglotPageService.injectI18n(tableColumn, locTextKey, toolTipKey); - return tableColumn; - } - - public TabFolder tabFolderLocalized(final Composite parent) { - final TabFolder tabs = new TabFolder(parent, SWT.NONE); - this.polyglotPageService.injectI18n(tabs); - return tabs; - } - - public TabItem tabItemLocalized( - final TabFolder parent, - final LocTextKey locTextKey) { - - return this.tabItemLocalized(parent, locTextKey, null); - } - - public TabItem tabItemLocalized( - final TabFolder parent, - final LocTextKey locTextKey, - final LocTextKey toolTipKey) { - - final TabItem tabItem = new TabItem(parent, SWT.NONE); - this.polyglotPageService.injectI18n(tabItem, locTextKey, toolTipKey); - return tabItem; - } - - public Label labelSeparator(final Composite parent) { - final Label label = new Label(parent, SWT.SEPARATOR | SWT.HORIZONTAL); - final GridData data = new GridData(SWT.FILL, SWT.TOP, true, false); - label.setLayoutData(data); - return label; - } - - public Label imageButton( - final ImageIcon type, - final Composite parent, - final LocTextKey toolTip) { - - return this.imageButton(type, parent, toolTip, null); - } - - public Label imageButton( - final ImageIcon type, - final Composite parent, - final LocTextKey toolTip, - final Listener listener) { - - final Label imageButton = labelLocalized(parent, (LocTextKey) null, toolTip); - imageButton.setData(RWT.CUSTOM_VARIANT, CustomVariant.IMAGE_BUTTON.name()); - imageButton.setImage(type.getImage(parent.getDisplay())); - if (listener != null) { - imageButton.addListener(SWT.MouseDown, listener); - } - return imageButton; - } - - public Selection selectionLocalized( - final Selection.Type type, - final Composite parent, - final Supplier>> itemsSupplier) { - - return this.selectionLocalized(type, parent, itemsSupplier, null, null); - } - - public Selection selectionLocalized( - final Selection.Type type, - final Composite parent, - final Supplier>> itemsSupplier, - final LocTextKey toolTipTextKey) { - - return this.selectionLocalized(type, parent, itemsSupplier, toolTipTextKey, null); - } - - public Selection selectionLocalized( - final Selection.Type type, - final Composite parent, - final Supplier>> itemsSupplier, - final LocTextKey toolTipTextKey, - final Supplier>> itemsToolTipSupplier) { - - return selectionLocalized(type, parent, itemsSupplier, toolTipTextKey, itemsToolTipSupplier, null); - } - - public Selection selectionLocalized( - final Selection.Type type, - final Composite parent, - final Supplier>> itemsSupplier, - final LocTextKey toolTipTextKey, - final Supplier>> itemsToolTipSupplier, - final String actionLocTextPrefix) { - - final Selection selection; - switch (type) { - case SINGLE: - selection = new SingleSelection(parent, SWT.READ_ONLY); - break; - case SINGLE_COMBO: - selection = new SingleSelection(parent, SWT.NONE); - break; - case RADIO: - selection = new RadioSelection(parent); - break; - case MULTI: - selection = new MultiSelection(parent); - break; - case MULTI_COMBO: - selection = new MultiSelectionCombo( - parent, - this, - actionLocTextPrefix, - // NOTE parent would work for firefox but on IE and Chrome only parent.getParent().getParent() works - parent.getParent().getParent()); - break; - case MULTI_CHECKBOX: - selection = new MultiSelectionCheckbox(parent); - break; - case COLOR: - selection = new ColorSelection(parent, this, actionLocTextPrefix); - break; - default: - throw new IllegalArgumentException("Unsupported Selection.Type: " + type); - } - - if (itemsSupplier != null) { - final Consumer updateFunction = ss -> { - try { - ss.applyNewMapping(itemsSupplier.get()); - if (toolTipTextKey != null) { - ss.setToolTipText(Utils.formatLineBreaks(this.i18nSupport.getText(toolTipTextKey))); - } - if (itemsToolTipSupplier != null) { - ss.applyToolTipsForItems(itemsToolTipSupplier.get()); - } - } catch (final Exception e) { - log.error("Unexpected error while trying to apply localization to selection widget", e); - } - }; - selection.adaptToControl().setData(POLYGLOT_WIDGET_FUNCTION_KEY, updateFunction); - updateFunction.accept(selection); - } - - return selection; - } - - public DateTime dateSelector(final Composite parent) { - RWT.setLocale(Locale.GERMANY); - final GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, true); - final DateTime dateTime = new DateTime(parent, SWT.DATE | SWT.BORDER | SWT.DROP_DOWN); - dateTime.setLayoutData(gridData); - return dateTime; - } - - public DateTime timeSelector(final Composite parent) { - final GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, true); - final DateTime dateTime = new DateTime(parent, SWT.TIME | SWT.BORDER | SWT.SHORT); - dateTime.setLayoutData(gridData); - return dateTime; - } - - public DateTime timeSelectorWithSeconds(final Composite parent) { - final GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, true); - final DateTime dateTime = new DateTime(parent, SWT.TIME | SWT.BORDER | SWT.MEDIUM); - dateTime.setLayoutData(gridData); - return dateTime; - } - - public ColorDialog getColorDialog(final Composite parent) { - return new ColorDialog(parent.getShell(), SWT.NONE); - } - - public ThresholdList thresholdList( - final Composite parent, - final Composite updateAnchor, - final Collection thresholds, - final Supplier indicatorTypeSupplier) { - - final ThresholdList thresholdList = new ThresholdList( - parent, - updateAnchor, - this, - indicatorTypeSupplier); - if (thresholds != null) { - thresholdList.setThresholds(thresholds); - } - return thresholdList; - } - - public ImageUploadSelection logoImageUploadLocalized( - final Composite parent, - final LocTextKey locTextKey, - final boolean readonly) { - - return imageUploadLocalized( - parent, - locTextKey, - readonly, - DefaultPageLayout.LOGO_IMAGE_MAX_WIDTH, - DefaultPageLayout.LOGO_IMAGE_MAX_HEIGHT); - } - - public ImageUploadSelection imageUploadLocalized( - final Composite parent, - final LocTextKey locTextKey, - final boolean readonly, - final int maxWidth, - final int maxHeight) { - - final ImageUploadSelection imageUpload = new ImageUploadSelection( - parent, - this.serverPushService, - this.i18nSupport, - readonly, - maxWidth, - maxHeight); - - this.polyglotPageService.injectI18n(imageUpload, locTextKey); - return imageUpload; - } - - public FileUploadSelection fileUploadSelection( - final Composite parent, - final boolean readonly, - final Collection supportedFiles) { - - final FileUploadSelection fileUploadSelection = - new FileUploadSelection(parent, this.i18nSupport, readonly); - - if (supportedFiles != null) { - supportedFiles.forEach(ext -> fileUploadSelection.withSupportFor(ext)); - } - return fileUploadSelection; - } - - public static void setTestId(final Widget widget, final String value) { - setAttribute(widget, ADD_HTML_ATTR_TEST_ID, value); - } - - public static void setARIARole(final Widget widget, final String value) { - setAttribute(widget, ADD_HTML_ATTR_ARIA_ROLE, value); - } - - private static void setAttribute(final Widget widget, final String name, final String value) { - if (!widget.isDisposed()) { - final String $el = widget instanceof Text ? "$input" : "$el"; - final String id = WidgetUtil.getId(widget); - exec("rap.getObject( '", id, "' ).", $el, ".attr( '", name, "', '", value, "' );"); - } - } - - private static void exec(final String... strings) { - final StringBuilder builder = new StringBuilder(); - builder.append("try{"); - for (final String str : strings) { - builder.append(str); - } - builder.append("}catch(e){}"); - final JavaScriptExecutor executor = RWT.getClient().getService(JavaScriptExecutor.class); - executor.execute(builder.toString()); - } - -} +/* + * Copyright (c) 2018 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 static ch.ethz.seb.sebserver.gui.service.i18n.PolyglotPageService.POLYGLOT_WIDGET_FUNCTION_KEY; + +import java.io.InputStream; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import org.eclipse.rap.rwt.RWT; +import org.eclipse.rap.rwt.client.service.JavaScriptExecutor; +import org.eclipse.rap.rwt.widgets.WidgetUtil; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Device; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.ColorDialog; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.DateTime; +import org.eclipse.swt.widgets.Group; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.TabFolder; +import org.eclipse.swt.widgets.TabItem; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableColumn; +import org.eclipse.swt.widgets.Text; +import org.eclipse.swt.widgets.Tree; +import org.eclipse.swt.widgets.TreeItem; +import org.eclipse.swt.widgets.Widget; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; + +import ch.ethz.seb.sebserver.gbl.model.exam.Indicator.IndicatorType; +import ch.ethz.seb.sebserver.gbl.model.exam.Indicator.Threshold; +import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; +import ch.ethz.seb.sebserver.gbl.util.Tuple; +import ch.ethz.seb.sebserver.gbl.util.Utils; +import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition; +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.i18n.PolyglotPageService; +import ch.ethz.seb.sebserver.gui.service.page.PageService; +import ch.ethz.seb.sebserver.gui.service.page.impl.DefaultPageLayout; +import ch.ethz.seb.sebserver.gui.service.push.ServerPushService; + +@Lazy +@Service +@GuiProfile +public class WidgetFactory { + + private static final String ADD_HTML_ATTR_ARIA_ROLE = "role"; + private static final String ADD_HTML_ATTR_TEST_ID = "test-id"; + + private static final Logger log = LoggerFactory.getLogger(WidgetFactory.class); + + public static final int TEXT_AREA_INPUT_MIN_HEIGHT = 100; + public static final int TEXT_INPUT_MIN_HEIGHT = 24; + + public enum ImageIcon { + MAXIMIZE("maximize.png"), + MINIMIZE("minimize.png"), + MANDATORY("mandatory.png"), + ADD("add.png"), + REMOVE("remove.png"), + ADD_BOX("add_box.png"), + ADD_BOX_WHITE("add_box_w.png"), + REMOVE_BOX("remove_box.png"), + REMOVE_BOX_WHITE("remove_box_w.png"), + EDIT("edit.png"), + EDIT_SETTINGS("settings.png"), + TEST("test.png"), + COPY("copy.png"), + IMPORT("import.png"), + CANCEL("cancel.png"), + CANCEL_EDIT("cancelEdit.png"), + SHOW("show.png"), + ACTIVE("active.png"), + INACTIVE("inactive.png"), + TOGGLE_ON("toggle_on.png"), + TOGGLE_OFF("toggle_off.png"), + SWITCH("switch.png"), + YES("yes.png"), + NO("no.png"), + SAVE("save.png"), + EXPORT("export.png"), + SECURE("secure.png"), + NEW("new.png"), + DELETE("delete.png"), + SEARCH("lens.png"), + UNDO("undo.png"), + COLOR("color.png"), + USER("user.png"), + INSTITUTION("institution.png"), + LMS_SETUP("lmssetup.png"), + INDICATOR("indicator.png"), + TEMPLATE("template.png"), + DISABLE("disable.png"), + SEND_QUIT("send-quit.png"), + HELP("help.png"), + LOCK("lock.png"), + UNLOCK("unlock.png"), + RESTRICTION("restriction.png"), + VISIBILITY("visibility.png"), + VISIBILITY_OFF("visibility_off.png"); + + public String fileName; + private ImageData image = null; + private ImageData greyedImage = null; + + private ImageIcon(final String fileName) { + this.fileName = fileName; + } + + public Image getImage(final Device device) { + if (this.image == null) { + try { + final InputStream resourceAsStream = + WidgetFactory.class.getResourceAsStream("/static/images/" + this.fileName); + this.image = new ImageData(resourceAsStream); + } catch (final Exception e) { + log.error("Failed to load resource image: {}", this.fileName, e); + } + } + + return new Image(device, this.image); + } + + public Image getGreyedImage(final Device device) { + if (this.greyedImage == null) { + try { + final InputStream resourceAsStream = + WidgetFactory.class.getResourceAsStream("/static/images/" + this.fileName); + this.greyedImage = new ImageData(resourceAsStream); + this.greyedImage.alpha = -1; + for (int y = 0; y < this.greyedImage.height; y++) { + for (int x = 0; x < this.greyedImage.width; x++) { + this.greyedImage.setAlpha(x, y, this.greyedImage.getAlpha(x, y) / 3); + } + } + } catch (final Exception e) { + log.error("Failed to load resource image: {}", this.fileName, e); + } + } + + return new Image(device, this.greyedImage); + } + } + + public enum CustomVariant { + TEXT_H1("h1"), + TEXT_H2("h2"), + TEXT_H3("h3"), + IMAGE_BUTTON("imageButton"), + TEXT_ACTION("action"), + TEXT_READONLY("readonlyText"), + + FORM_CENTER("form-center"), + SELECTION("selection"), + SELECTED("selected"), + + ACTIVITY_TREE_SECTION("treesection"), + + FOOTER("footer"), + TITLE_LABEL("head"), + + MESSAGE("message"), + ERROR("error"), + WARNING("warning"), + CONFIG_INPUT_READONLY("inputreadonly"), + + DARK_COLOR_LABEL("colordark"), + LIGHT_COLOR_LABEL("colorlight"), + + LOGIN("login"), + LOGIN_BACK("login-back"), + SCROLL("scroll"), + + LIST_NAVIGATION("list-nav"), + PLAIN_PWD("pwdplain"), + COLOR_BOX("colorbox") + + ; + + public final String key; + + private CustomVariant(final String key) { + this.key = key; + } + } + + private final PolyglotPageService polyglotPageService; + private final I18nSupport i18nSupport; + private final ServerPushService serverPushService; + + public WidgetFactory( + final PolyglotPageService polyglotPageService, + final ServerPushService serverPushService) { + + this.polyglotPageService = polyglotPageService; + this.i18nSupport = polyglotPageService.getI18nSupport(); + this.serverPushService = serverPushService; + } + + public I18nSupport getI18nSupport() { + return this.i18nSupport; + } + + public Composite defaultPageLayout(final Composite parent) { + final Composite content = new Composite(parent, SWT.NONE); + final GridLayout contentLayout = new GridLayout(); + contentLayout.marginLeft = 10; + content.setLayout(contentLayout); + final GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, true); + content.setLayoutData(gridData); + return content; + } + + public Composite defaultPageLayout(final Composite parent, final LocTextKey title) { + final Composite defaultPageLayout = defaultPageLayout(parent); + final Label labelLocalizedTitle = labelLocalizedTitle(defaultPageLayout, title); + final GridData gridData = new GridData(SWT.FILL, SWT.TOP, true, false); + labelLocalizedTitle.setLayoutData(gridData); + return defaultPageLayout; + } + + public Composite defaultPageLayout( + final Composite parent, + final LocTextKey title, + final ActionDefinition actionDefinition) { + + final Composite defaultPageLayout = defaultPageLayout(parent); + final Label labelLocalizedTitle = labelLocalizedTitle(defaultPageLayout, title); + labelLocalizedTitle.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false)); + return defaultPageLayout; + } + + public Composite formGrid(final Composite parent, final int rows) { + final Composite grid = new Composite(parent, SWT.NONE); + final GridLayout layout = new GridLayout(rows, true); + layout.horizontalSpacing = 10; + layout.verticalSpacing = 10; + layout.marginBottom = 10; + layout.marginLeft = 10; + layout.marginTop = 0; + grid.setLayout(layout); + grid.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false)); + return grid; + } + + /** Use this to create a scrolled Composite for usual popup forms + * + * @param parent The parent Composite + * @return the scrolled Composite to add the form content */ + public Composite createPopupScrollComposite(final Composite parent) { + final Composite grid = PageService.createManagedVScrolledComposite( + parent, + scrolledComposite -> { + final Composite g = new Composite(scrolledComposite, SWT.NONE); + g.setLayout(new GridLayout()); + g.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false)); + return g; + }, + false); + return grid; + } + + public Composite createWarningPanel(final Composite parent) { + final Composite composite = new Composite(parent, SWT.NONE); + composite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false)); + final GridLayout gridLayout = new GridLayout(1, true); + gridLayout.marginWidth = 20; + gridLayout.marginHeight = 20; + composite.setLayout(gridLayout); + composite.setData(RWT.CUSTOM_VARIANT, CustomVariant.WARNING.key); + return composite; + } + + public Button buttonLocalized(final Composite parent, final String locTextKey) { + final Button button = new Button(parent, SWT.NONE); + this.polyglotPageService.injectI18n(button, new LocTextKey(locTextKey)); + return button; + } + + public Button buttonLocalized(final Composite parent, final LocTextKey locTextKey) { + final Button button = new Button(parent, SWT.NONE); + this.polyglotPageService.injectI18n(button, locTextKey); + return button; + } + + public Button buttonLocalized(final Composite parent, final CustomVariant variant, final String locTextKey) { + final Button button = new Button(parent, SWT.NONE); + this.polyglotPageService.injectI18n(button, new LocTextKey(locTextKey)); + button.setData(RWT.CUSTOM_VARIANT, variant.key); + return button; + } + + public Button buttonLocalized( + final Composite parent, + final int type, + final LocTextKey locTextKey, + final LocTextKey toolTipKey) { + + final Button button = new Button(parent, type); + this.polyglotPageService.injectI18n(button, locTextKey, toolTipKey); + return button; + } + + public Label label(final Composite parent, final String text) { + final Label label = new Label(parent, SWT.NONE); + label.setText(text); + return label; + } + + public Label labelLocalized(final Composite parent, final String locTextKey) { + final Label label = new Label(parent, SWT.NONE); + this.polyglotPageService.injectI18n(label, new LocTextKey(locTextKey)); + return label; + } + + public Label labelLocalized(final Composite parent, final LocTextKey locTextKey) { + final Label label = new Label(parent, SWT.NONE); + this.polyglotPageService.injectI18n(label, locTextKey); + return label; + } + + public Label labelLocalized(final Composite parent, final CustomVariant variant, final LocTextKey locTextKey) { + final Label label = new Label(parent, SWT.NONE); + this.polyglotPageService.injectI18n(label, locTextKey); + label.setData(RWT.CUSTOM_VARIANT, variant.key); + label.setData(RWT.MARKUP_ENABLED, true); + return label; + } + + public Label labelLocalized( + final Composite parent, + final LocTextKey locTextKey, + final LocTextKey locToolTextKey) { + + final Label label = new Label(parent, SWT.NONE); + this.polyglotPageService.injectI18n(label, locTextKey, locToolTextKey); + return label; + } + + public Label labelLocalized( + final Composite parent, + final CustomVariant variant, + final LocTextKey locTextKey, + final LocTextKey locToolTextKey) { + + final Label label = new Label(parent, SWT.NONE); + this.polyglotPageService.injectI18n(label, locTextKey, locToolTextKey); + label.setData(RWT.CUSTOM_VARIANT, variant.key); + return label; + } + + public Label labelLocalizedTitle(final Composite content, final LocTextKey locTextKey) { + final Label labelLocalized = labelLocalized(content, CustomVariant.TEXT_H1, locTextKey); + labelLocalized.setLayoutData(new GridData(SWT.TOP, SWT.LEFT, true, false)); + return labelLocalized; + } + + public Text textInput(final Composite content) { + return textInput(content, false, false); + } + + public Text textLabel(final Composite content) { + return textInput(content, false, true); + } + + public Text passwordInput(final Composite content) { + return textInput(content, true, false); + } + + public Text textAreaInput(final Composite content, final boolean readonly) { + return readonly + ? new Text(content, SWT.LEFT | SWT.MULTI) + : new Text(content, SWT.LEFT | SWT.BORDER | SWT.MULTI); + } + + public Text textInput(final Composite content, final boolean password, final boolean readonly) { + return readonly + ? new Text(content, SWT.LEFT) + : new Text(content, (password) + ? SWT.LEFT | SWT.BORDER | SWT.PASSWORD + : SWT.LEFT | SWT.BORDER); + } + + public Text numberInput(final Composite content, final Consumer numberCheck) { + return numberInput(content, numberCheck, false); + } + + public Text numberInput(final Composite content, final Consumer numberCheck, final boolean readonly) { + if (readonly) { + return new Text(content, SWT.LEFT | SWT.READ_ONLY); + } + + final Text numberInput = new Text(content, SWT.RIGHT | SWT.BORDER); + if (numberCheck != null) { + numberInput.addListener(SWT.Verify, event -> { + final String value = event.text; + try { + numberCheck.accept(value); + } catch (final Exception e) { + event.doit = false; + } + }); + } + return numberInput; + } + + public Group groupLocalized( + final Composite parent, + final int columns, + final LocTextKey locTextKey) { + + return groupLocalized(parent, columns, locTextKey, null); + } + + public Group groupLocalized( + final Composite parent, + final int columns, + final LocTextKey locTextKey, + final LocTextKey locTooltipKey) { + + final Group group = new Group(parent, SWT.NONE); + final GridLayout gridLayout = new GridLayout(columns, true); + gridLayout.verticalSpacing = 0; + gridLayout.horizontalSpacing = 0; + gridLayout.marginHeight = 0; + group.setLayout(gridLayout); + + this.polyglotPageService.injectI18n(group, locTextKey, locTooltipKey); + return group; + } + + public Tree treeLocalized(final Composite parent, final int style) { + final Tree tree = new Tree(parent, style); + this.polyglotPageService.injectI18n(tree); + return tree; + } + + public TreeItem treeItemLocalized(final Tree parent, final String locTextKey) { + final TreeItem item = new TreeItem(parent, SWT.NONE); + this.polyglotPageService.injectI18n(item, new LocTextKey(locTextKey)); + return item; + } + + public TreeItem treeItemLocalized(final Tree parent, final LocTextKey locTextKey) { + final TreeItem item = new TreeItem(parent, SWT.NONE); + this.polyglotPageService.injectI18n(item, locTextKey); + return item; + } + + public TreeItem treeItemLocalized(final TreeItem parent, final String locTextKey) { + final TreeItem item = new TreeItem(parent, SWT.NONE); + this.polyglotPageService.injectI18n(item, new LocTextKey(locTextKey)); + return item; + } + + public TreeItem treeItemLocalized(final TreeItem parent, final LocTextKey locTextKey) { + final TreeItem item = new TreeItem(parent, SWT.NONE); + this.polyglotPageService.injectI18n(item, locTextKey); + return item; + } + + public Table tableLocalized(final Composite parent) { + final Table table = new Table(parent, SWT.NO_SCROLL); + this.polyglotPageService.injectI18n(table); + return table; + } + + public Table tableLocalized(final Composite parent, final int style) { + final Table table = new Table(parent, style); + this.polyglotPageService.injectI18n(table); + return table; + } + + public TableColumn tableColumnLocalized( + final Table table, + final LocTextKey locTextKey) { + + return tableColumnLocalized(table, locTextKey, null); + } + + public TableColumn tableColumnLocalized( + final Table table, + final LocTextKey locTextKey, + final LocTextKey toolTipKey) { + + final TableColumn tableColumn = new TableColumn(table, SWT.NONE); + this.polyglotPageService.injectI18n(tableColumn, locTextKey, toolTipKey); + return tableColumn; + } + + public TabFolder tabFolderLocalized(final Composite parent) { + final TabFolder tabs = new TabFolder(parent, SWT.NONE); + this.polyglotPageService.injectI18n(tabs); + return tabs; + } + + public TabItem tabItemLocalized( + final TabFolder parent, + final LocTextKey locTextKey) { + + return this.tabItemLocalized(parent, locTextKey, null); + } + + public TabItem tabItemLocalized( + final TabFolder parent, + final LocTextKey locTextKey, + final LocTextKey toolTipKey) { + + final TabItem tabItem = new TabItem(parent, SWT.NONE); + this.polyglotPageService.injectI18n(tabItem, locTextKey, toolTipKey); + return tabItem; + } + + public Label labelSeparator(final Composite parent) { + final Label label = new Label(parent, SWT.SEPARATOR | SWT.HORIZONTAL); + final GridData data = new GridData(SWT.FILL, SWT.TOP, true, false); + label.setLayoutData(data); + return label; + } + + public Label imageButton( + final ImageIcon type, + final Composite parent, + final LocTextKey toolTip) { + + return this.imageButton(type, parent, toolTip, null); + } + + public Label imageButton( + final ImageIcon type, + final Composite parent, + final LocTextKey toolTip, + final Listener listener) { + + final Label imageButton = labelLocalized(parent, (LocTextKey) null, toolTip); + imageButton.setData(RWT.CUSTOM_VARIANT, CustomVariant.IMAGE_BUTTON.name()); + imageButton.setImage(type.getImage(parent.getDisplay())); + if (listener != null) { + imageButton.addListener(SWT.MouseDown, listener); + } + return imageButton; + } + + public Selection selectionLocalized( + final Selection.Type type, + final Composite parent, + final Supplier>> itemsSupplier) { + + return this.selectionLocalized(type, parent, itemsSupplier, null, null); + } + + public Selection selectionLocalized( + final Selection.Type type, + final Composite parent, + final Supplier>> itemsSupplier, + final LocTextKey toolTipTextKey) { + + return this.selectionLocalized(type, parent, itemsSupplier, toolTipTextKey, null); + } + + public Selection selectionLocalized( + final Selection.Type type, + final Composite parent, + final Supplier>> itemsSupplier, + final LocTextKey toolTipTextKey, + final Supplier>> itemsToolTipSupplier) { + + return selectionLocalized(type, parent, itemsSupplier, toolTipTextKey, itemsToolTipSupplier, null); + } + + public Selection selectionLocalized( + final Selection.Type type, + final Composite parent, + final Supplier>> itemsSupplier, + final LocTextKey toolTipTextKey, + final Supplier>> itemsToolTipSupplier, + final String actionLocTextPrefix) { + + final Selection selection; + switch (type) { + case SINGLE: + selection = new SingleSelection(parent, SWT.READ_ONLY); + break; + case SINGLE_COMBO: + selection = new SingleSelection(parent, SWT.NONE); + break; + case RADIO: + selection = new RadioSelection(parent); + break; + case MULTI: + selection = new MultiSelection(parent); + break; + case MULTI_COMBO: + selection = new MultiSelectionCombo( + parent, + this, + actionLocTextPrefix, + // NOTE parent would work for firefox but on IE and Chrome only parent.getParent().getParent() works + parent.getParent().getParent()); + break; + case MULTI_CHECKBOX: + selection = new MultiSelectionCheckbox(parent); + break; + case COLOR: + selection = new ColorSelection(parent, this, actionLocTextPrefix); + break; + default: + throw new IllegalArgumentException("Unsupported Selection.Type: " + type); + } + + if (itemsSupplier != null) { + final Consumer updateFunction = ss -> { + try { + ss.applyNewMapping(itemsSupplier.get()); + if (toolTipTextKey != null) { + ss.setToolTipText(Utils.formatLineBreaks(this.i18nSupport.getText(toolTipTextKey))); + } + if (itemsToolTipSupplier != null) { + ss.applyToolTipsForItems(itemsToolTipSupplier.get()); + } + } catch (final Exception e) { + log.error("Unexpected error while trying to apply localization to selection widget", e); + } + }; + selection.adaptToControl().setData(POLYGLOT_WIDGET_FUNCTION_KEY, updateFunction); + updateFunction.accept(selection); + } + + return selection; + } + + public DateTime dateSelector(final Composite parent) { + RWT.setLocale(Locale.GERMANY); + final GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, true); + final DateTime dateTime = new DateTime(parent, SWT.DATE | SWT.BORDER | SWT.DROP_DOWN); + dateTime.setLayoutData(gridData); + return dateTime; + } + + public DateTime timeSelector(final Composite parent) { + final GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, true); + final DateTime dateTime = new DateTime(parent, SWT.TIME | SWT.BORDER | SWT.SHORT); + dateTime.setLayoutData(gridData); + return dateTime; + } + + public DateTime timeSelectorWithSeconds(final Composite parent) { + final GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, true); + final DateTime dateTime = new DateTime(parent, SWT.TIME | SWT.BORDER | SWT.MEDIUM); + dateTime.setLayoutData(gridData); + return dateTime; + } + + public ColorDialog getColorDialog(final Composite parent) { + return new ColorDialog(parent.getShell(), SWT.NONE); + } + + public ThresholdList thresholdList( + final Composite parent, + final Composite updateAnchor, + final Collection thresholds, + final Supplier indicatorTypeSupplier) { + + final ThresholdList thresholdList = new ThresholdList( + parent, + updateAnchor, + this, + indicatorTypeSupplier); + if (thresholds != null) { + thresholdList.setThresholds(thresholds); + } + return thresholdList; + } + + public ImageUploadSelection logoImageUploadLocalized( + final Composite parent, + final LocTextKey locTextKey, + final boolean readonly) { + + return imageUploadLocalized( + parent, + locTextKey, + readonly, + DefaultPageLayout.LOGO_IMAGE_MAX_WIDTH, + DefaultPageLayout.LOGO_IMAGE_MAX_HEIGHT); + } + + public ImageUploadSelection imageUploadLocalized( + final Composite parent, + final LocTextKey locTextKey, + final boolean readonly, + final int maxWidth, + final int maxHeight) { + + final ImageUploadSelection imageUpload = new ImageUploadSelection( + parent, + this.serverPushService, + this.i18nSupport, + readonly, + maxWidth, + maxHeight); + + this.polyglotPageService.injectI18n(imageUpload, locTextKey); + return imageUpload; + } + + public FileUploadSelection fileUploadSelection( + final Composite parent, + final boolean readonly, + final Collection supportedFiles) { + + final FileUploadSelection fileUploadSelection = + new FileUploadSelection(parent, this.i18nSupport, readonly); + + if (supportedFiles != null) { + supportedFiles.forEach(ext -> fileUploadSelection.withSupportFor(ext)); + } + return fileUploadSelection; + } + + public static void setTestId(final Widget widget, final String value) { + setAttribute(widget, ADD_HTML_ATTR_TEST_ID, value); + } + + public static void setARIARole(final Widget widget, final String value) { + setAttribute(widget, ADD_HTML_ATTR_ARIA_ROLE, value); + } + + private static void setAttribute(final Widget widget, final String name, final String value) { + if (!widget.isDisposed()) { + final String $el = widget instanceof Text ? "$input" : "$el"; + final String id = WidgetUtil.getId(widget); + exec("rap.getObject( '", id, "' ).", $el, ".attr( '", name, "', '", value, "' );"); + } + } + + private static void exec(final String... strings) { + final StringBuilder builder = new StringBuilder(); + builder.append("try{"); + for (final String str : strings) { + builder.append(str); + } + builder.append("}catch(e){}"); + final JavaScriptExecutor executor = RWT.getClient().getService(JavaScriptExecutor.class); + executor.execute(builder.toString()); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/client/ClientCredentialServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/client/ClientCredentialServiceImpl.java index 64ac6852..e67ee155 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/client/ClientCredentialServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/client/ClientCredentialServiceImpl.java @@ -1,203 +1,143 @@ -/* - * 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.client; - -import java.io.UnsupportedEncodingException; -import java.nio.CharBuffer; -import java.security.SecureRandom; - -import org.apache.commons.lang3.RandomStringUtils; -import org.apache.commons.lang3.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.context.annotation.Lazy; -import org.springframework.core.env.Environment; -import org.springframework.security.crypto.encrypt.Encryptors; -import org.springframework.security.crypto.keygen.KeyGenerators; -import org.springframework.stereotype.Service; - -import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; -import ch.ethz.seb.sebserver.gbl.util.Result; - -@Lazy -@Service -@WebServiceProfile -public class ClientCredentialServiceImpl implements ClientCredentialService { - - private static final Logger log = LoggerFactory.getLogger(ClientCredentialServiceImpl.class); - - static final String SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY = "sebserver.webservice.internalSecret"; - - private final Environment environment; - - protected ClientCredentialServiceImpl(final Environment environment) { - this.environment = environment; - } - - @Override - public Result generatedClientCredentials() { - return Result.tryCatch(() -> { - try { - - return encryptClientCredentials( - generateClientId(), - generateClientSecret()); - - } catch (final UnsupportedEncodingException e) { - log.error("Error while trying to generate client credentials: ", e); - throw new RuntimeException("cause: ", e); - } - }); - } - - @Override - public ClientCredentials encryptClientCredentials( - final CharSequence clientIdPlaintext, - final CharSequence secretPlaintext, - final CharSequence accessTokenPlaintext) { - - final CharSequence secret = this.environment - .getProperty(SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY); - - return new ClientCredentials( - clientIdPlaintext, - (StringUtils.isNoneBlank(secretPlaintext)) - ? encrypt(secretPlaintext, secret).toString() - : null, - (StringUtils.isNoneBlank(accessTokenPlaintext)) - ? encrypt(accessTokenPlaintext, secret).toString() - : null); - } - - @Override - public CharSequence getPlainClientSecret(final ClientCredentials credentials) { - if (credentials == null || !credentials.hasSecret()) { - return null; - } - - final CharSequence secret = this.environment - .getProperty(SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY); - return this.decrypt(credentials.secret, secret); - } - - @Override - public CharSequence getPlainAccessToken(final ClientCredentials credentials) { - if (credentials == null || !credentials.hasAccessToken()) { - return null; - } - - final CharSequence secret = this.environment - .getProperty(SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY); - - return this.decrypt(credentials.accessToken, secret); - } - - @Override - public CharSequence encrypt(final CharSequence text) { - - final CharSequence secret = this.environment - .getProperty(SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY); - - return encrypt(text, secret); - } - - @Override - public CharSequence decrypt(final CharSequence text) { - - final CharSequence secret = this.environment - .getProperty(SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY); - - return decrypt(text, secret); - } - - CharSequence encrypt(final CharSequence text, final CharSequence secret) { - if (text == null) { - throw new IllegalArgumentException("Text has null reference"); - } - - if (secret == null) { - log.warn("No internal secret supplied: skip encryption"); - return text; - } - - try { - - final CharSequence salt = KeyGenerators.string().generateKey(); - final CharSequence cipher = Encryptors - .delux(secret, salt) - .encrypt(text.toString()); - - return new StringBuilder(cipher) - .append(salt); - - } catch (final Exception e) { - log.error("Failed to encrypt text: ", e); - throw e; - } - } - - CharSequence decrypt(final CharSequence cipher, final CharSequence secret) { - if (cipher == null) { - throw new IllegalArgumentException("Cipher has null reference"); - } - - if (secret == null) { - log.warn("No internal secret supplied: skip decryption"); - return cipher; - } - - try { - - final int length = cipher.length(); - final int cipherTextLength = length - 16; - final CharSequence salt = cipher.subSequence(cipherTextLength, length); - final CharSequence cipherText = cipher.subSequence(0, cipherTextLength); - - return Encryptors - .delux(secret, salt) - .decrypt(cipherText.toString()); - - } catch (final Exception e) { - log.error("Failed to decrypt text: ", e); - throw e; - } - } - - private final static char[] possibleCharacters = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789~`!@#$%^*()-_=+[{]}?" - .toCharArray(); - - public final static CharSequence generateClientId() { - return RandomStringUtils.random( - 16, 0, possibleCharacters.length - 1, false, false, - possibleCharacters, new SecureRandom()); - } - - public final static CharSequence generateClientSecret() throws UnsupportedEncodingException { - // TODO fine a better way to generate a random char array instead of using RandomStringUtils.random which uses a String - return RandomStringUtils.random( - 64, 0, possibleCharacters.length - 1, false, false, - possibleCharacters, new SecureRandom()); - } - - public final static void clearChars(final CharSequence sequence) { - if (sequence == null) { - return; - } - - if (sequence instanceof CharBuffer) { - ((CharBuffer) sequence).clear(); - return; - } - - throw new IllegalArgumentException( - "Cannot clear chars on CharSequence of type: " + sequence.getClass().getName()); - } - -} +/* + * 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.client; + +import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; +import ch.ethz.seb.sebserver.gbl.util.Cryptor; +import ch.ethz.seb.sebserver.gbl.util.Result; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Lazy; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Service; + +import java.io.UnsupportedEncodingException; +import java.nio.CharBuffer; +import java.security.SecureRandom; + +@Lazy +@Service +@WebServiceProfile +public class ClientCredentialServiceImpl implements ClientCredentialService { + + private static final Logger log = LoggerFactory.getLogger(ClientCredentialServiceImpl.class); + + private final Environment environment; + private final Cryptor cryptor; + + protected ClientCredentialServiceImpl( + final Environment environment, + final Cryptor cryptor) { + + this.environment = environment; + this.cryptor = cryptor; + } + + @Override + public Result generatedClientCredentials() { + return Result.tryCatch(() -> { + try { + + return encryptClientCredentials( + generateClientId(), + generateClientSecret()); + + } catch (final UnsupportedEncodingException e) { + log.error("Error while trying to generate client credentials: ", e); + throw new RuntimeException("cause: ", e); + } + }); + } + + @Override + public ClientCredentials encryptClientCredentials( + final CharSequence clientIdPlaintext, + final CharSequence secretPlaintext, + final CharSequence accessTokenPlaintext) { + + final CharSequence secret = this.environment + .getProperty(Cryptor.SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY); + + return new ClientCredentials( + clientIdPlaintext, + (StringUtils.isNoneBlank(secretPlaintext)) + ? Cryptor.encrypt(secretPlaintext, secret).toString() + : null, + (StringUtils.isNoneBlank(accessTokenPlaintext)) + ? Cryptor.encrypt(accessTokenPlaintext, secret).toString() + : null); + } + + @Override + public CharSequence getPlainClientSecret(final ClientCredentials credentials) { + if (credentials == null || !credentials.hasSecret()) { + return null; + } + + final CharSequence secret = this.environment + .getProperty(Cryptor.SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY); + return Cryptor.decrypt(credentials.secret, secret); + } + + @Override + public CharSequence getPlainAccessToken(final ClientCredentials credentials) { + if (credentials == null || !credentials.hasAccessToken()) { + return null; + } + + final CharSequence secret = this.environment + .getProperty(Cryptor.SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY); + + return Cryptor.decrypt(credentials.accessToken, secret); + } + + @Override + public CharSequence encrypt(final CharSequence text) { + return cryptor.encrypt(text); + } + + @Override + public CharSequence decrypt(final CharSequence text) { + return cryptor.decrypt(text); + } + + private final static char[] possibleCharacters = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789~`!@#$%^*()-_=+[{]}?" + .toCharArray(); + + public static CharSequence generateClientId() { + return RandomStringUtils.random( + 16, 0, possibleCharacters.length - 1, false, false, + possibleCharacters, new SecureRandom()); + } + + public static CharSequence generateClientSecret() throws UnsupportedEncodingException { + // TODO find a better way to generate a random char array instead of using RandomStringUtils.random which uses a String + return RandomStringUtils.random( + 64, 0, possibleCharacters.length - 1, false, false, + possibleCharacters, new SecureRandom()); + } + + public static void clearChars(final CharSequence sequence) { + if (sequence == null) { + return; + } + + if (sequence instanceof CharBuffer) { + ((CharBuffer) sequence).clear(); + return; + } + + throw new IllegalArgumentException( + "Cannot clear chars on CharSequence of type: " + sequence.getClass().getName()); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/.gitignore b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/.gitignore deleted file mode 100644 index c6a822f3..00000000 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/ExamDAO.java diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ExamConfigurationMapDAO.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ExamConfigurationMapDAO.java index 5dfccdc3..8079954a 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ExamConfigurationMapDAO.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ExamConfigurationMapDAO.java @@ -1,70 +1,70 @@ -/* - * 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.dao; - -import java.util.Collection; - -import ch.ethz.seb.sebserver.gbl.model.exam.ExamConfigurationMap; -import ch.ethz.seb.sebserver.gbl.util.Result; -import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkActionSupportDAO; - -public interface ExamConfigurationMapDAO extends - EntityDAO, - BulkActionSupportDAO { - - /** Get a specific ExamConfigurationMap by the mapping identifiers - * - * @param examId The Exam mapping identifier - * @param configurationNodeId the ConfigurationNode mapping identifier - * @return Result refer to the ExamConfigurationMap with specified mapping or to an exception if happened */ - Result byMapping(Long examId, Long configurationNodeId); - - /** Get the password cipher of a specific ExamConfigurationMap by the mapping identifiers - * - * @param examId The Exam mapping identifier - * @param configurationNodeId the ConfigurationNode mapping identifier - * @return Result refer to the password cipher of specified mapping or to an exception if happened */ - Result getConfigPasswortCipher(Long examId, Long configurationNodeId); - - /** Get the ConfigurationNode identifier of the default Exam Configuration of - * the Exam with specified identifier. - * - * @param examId The Exam identifier - * @return ConfigurationNode identifier of the default Exam Configuration of - * the Exam with specified identifier */ - Result getDefaultConfigurationNode(Long examId); - - /** Get the ConfigurationNode identifier of the Exam Configuration of - * the Exam for a specified user identifier. - * - * @param examId The Exam identifier - * @param userId the user identifier - * @return ConfigurationNode identifier of the Exam Configuration of - * the Exam for a specified user identifier */ - Result getUserConfigurationNodeId(final Long examId, final String userId); - - /** Get a list of all ConfigurationNode identifiers of configurations that currently are attached to a given Exam - * - * @param examId the Exam identifier - * @return Result refers to a list of ConfigurationNode identifiers or refer to an error if happened */ - Result> getConfigurationNodeIds(Long examId); - - /** Get all id of Exams that has a relation to the given configuration id. - * - * @param configurationNodeId the configuration node identifier (PK) - * @return Result referencing the List of exam identifiers (PK) for a given configuration node identifier */ - Result> getExamIdsForConfigNodeId(Long configurationNodeId); - - /** Get all id of Exams that has a relation to the given configuration id. - * - * @param configurationId - * @return Result referencing the List of exam identifiers (PK) for a given configuration identifier */ - Result> getExamIdsForConfigId(Long configurationId); - -} +/* + * 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.dao; + +import java.util.Collection; + +import ch.ethz.seb.sebserver.gbl.model.exam.ExamConfigurationMap; +import ch.ethz.seb.sebserver.gbl.util.Result; +import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkActionSupportDAO; + +public interface ExamConfigurationMapDAO extends + EntityDAO, + BulkActionSupportDAO { + + /** Get a specific ExamConfigurationMap by the mapping identifiers + * + * @param examId The Exam mapping identifier + * @param configurationNodeId the ConfigurationNode mapping identifier + * @return Result refer to the ExamConfigurationMap with specified mapping or to an exception if happened */ + Result byMapping(Long examId, Long configurationNodeId); + + /** Get the password cipher of a specific ExamConfigurationMap by the mapping identifiers + * + * @param examId The Exam mapping identifier + * @param configurationNodeId the ConfigurationNode mapping identifier + * @return Result refer to the password cipher of specified mapping or to an exception if happened */ + Result getConfigPasswordCipher(Long examId, Long configurationNodeId); + + /** Get the ConfigurationNode identifier of the default Exam Configuration of + * the Exam with specified identifier. + * + * @param examId The Exam identifier + * @return ConfigurationNode identifier of the default Exam Configuration of + * the Exam with specified identifier */ + Result getDefaultConfigurationNode(Long examId); + + /** Get the ConfigurationNode identifier of the Exam Configuration of + * the Exam for a specified user identifier. + * + * @param examId The Exam identifier + * @param userId the user identifier + * @return ConfigurationNode identifier of the Exam Configuration of + * the Exam for a specified user identifier */ + Result getUserConfigurationNodeId(final Long examId, final String userId); + + /** Get a list of all ConfigurationNode identifiers of configurations that currently are attached to a given Exam + * + * @param examId the Exam identifier + * @return Result refers to a list of ConfigurationNode identifiers or refer to an error if happened */ + Result> getConfigurationNodeIds(Long examId); + + /** Get all id of Exams that has a relation to the given configuration id. + * + * @param configurationNodeId the configuration node identifier (PK) + * @return Result referencing the List of exam identifiers (PK) for a given configuration node identifier */ + Result> getExamIdsForConfigNodeId(Long configurationNodeId); + + /** Get all id of Exams that has a relation to the given configuration id. + * + * @param configurationId + * @return Result referencing the List of exam identifiers (PK) for a given configuration identifier */ + Result> getExamIdsForConfigId(Long configurationId); + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/SebClientConfigDAO.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/SebClientConfigDAO.java index c5b4f0a3..c5a9fccd 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/SebClientConfigDAO.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/SebClientConfigDAO.java @@ -1,63 +1,63 @@ -/* - * 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.dao; - -import java.util.Collection; -import java.util.Set; - -import org.springframework.cache.annotation.CacheEvict; - -import ch.ethz.seb.sebserver.gbl.model.EntityKey; -import ch.ethz.seb.sebserver.gbl.model.sebconfig.SebClientConfig; -import ch.ethz.seb.sebserver.gbl.util.Result; -import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkActionSupportDAO; -import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentials; -import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ClientConfigService; - -/** Concrete EntityDAO interface of SebClientConfig entities */ -public interface SebClientConfigDAO extends - ActivatableEntityDAO, - BulkActionSupportDAO { - - /** Get a SebClientConfig by specified client identifier - * - * @param clientName the client name - * @return Result refer to the SebClientConfig for client or refer to an error if happened */ - Result byClientName(String clientName); - - /** Get the configured ClientCredentials for a given SebClientConfig. - * The ClientCredentials are still encoded as they are on DB storage - * - * @param modelId the model identifier of the SebClientConfig to get the ClientCredentials for - * @return the configured ClientCredentials for a given SebClientConfig */ - Result getSebClientCredentials(String modelId); - - /** Get the stored encrypted configuration password from a specified SEB client configuration. - * The SEB client configuration password is used to encrypt a SEB Client Configuration - * - * @param modelId the model - * @return encrypted configuration password */ - Result getConfigPasswortCipher(String modelId); - - /** Get the stored encrypted configuration password from a specified SEB client configuration. - * The SEB client configuration password is used to encrypt a SEB Client Configuration. - * - * The SEB client configuration must be active otherwise a error is returned - * - * @param clientName the client name - * @return encrypted configuration password */ - Result getConfigPasswortCipherByClientName(String clientName); - - @Override - @CacheEvict( - cacheNames = ClientConfigService.EXAM_CLIENT_DETAILS_CACHE, - allEntries = true) - Result> delete(Set all); - -} +/* + * 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.dao; + +import java.util.Collection; +import java.util.Set; + +import org.springframework.cache.annotation.CacheEvict; + +import ch.ethz.seb.sebserver.gbl.model.EntityKey; +import ch.ethz.seb.sebserver.gbl.model.sebconfig.SebClientConfig; +import ch.ethz.seb.sebserver.gbl.util.Result; +import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkActionSupportDAO; +import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentials; +import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ClientConfigService; + +/** Concrete EntityDAO interface of SebClientConfig entities */ +public interface SebClientConfigDAO extends + ActivatableEntityDAO, + BulkActionSupportDAO { + + /** Get a SebClientConfig by specified client identifier + * + * @param clientName the client name + * @return Result refer to the SebClientConfig for client or refer to an error if happened */ + Result byClientName(String clientName); + + /** Get the configured ClientCredentials for a given SebClientConfig. + * The ClientCredentials are still encoded as they are on DB storage + * + * @param modelId the model identifier of the SebClientConfig to get the ClientCredentials for + * @return the configured ClientCredentials for a given SebClientConfig */ + Result getSebClientCredentials(String modelId); + + /** Get the stored encrypted configuration password from a specified SEB client configuration. + * The SEB client configuration password is used to encrypt a SEB Client Configuration + * + * @param modelId the model + * @return encrypted configuration password */ + Result getConfigPasswordCipher(String modelId); + + /** Get the stored encrypted configuration password from a specified SEB client configuration. + * The SEB client configuration password is used to encrypt a SEB Client Configuration. + * + * The SEB client configuration must be active otherwise a error is returned + * + * @param clientName the client name + * @return encrypted configuration password */ + Result getConfigPasswordCipherByClientName(String clientName); + + @Override + @CacheEvict( + cacheNames = ClientConfigService.EXAM_CLIENT_DETAILS_CACHE, + allEntries = true) + Result> delete(Set all); + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ClientConnectionDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ClientConnectionDAOImpl.java index b45efb52..eee68c85 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ClientConnectionDAOImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ClientConnectionDAOImpl.java @@ -1,266 +1,264 @@ -/* - * 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.dao.impl; - -import static org.mybatis.dynamic.sql.SqlBuilder.isEqualToWhenPresent; -import static org.mybatis.dynamic.sql.SqlBuilder.isIn; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Set; -import java.util.function.Predicate; -import java.util.stream.Collectors; - -import org.mybatis.dynamic.sql.SqlBuilder; -import org.springframework.context.annotation.Lazy; -import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; - -import ch.ethz.seb.sebserver.gbl.api.EntityType; -import ch.ethz.seb.sebserver.gbl.model.EntityKey; -import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; -import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus; -import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; -import ch.ethz.seb.sebserver.gbl.util.Result; -import ch.ethz.seb.sebserver.gbl.util.Utils; -import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientConnectionRecordDynamicSqlSupport; -import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientConnectionRecordMapper; -import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientEventRecordDynamicSqlSupport; -import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientEventRecordMapper; -import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientConnectionRecord; -import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientConnectionDAO; -import ch.ethz.seb.sebserver.webservice.servicelayer.dao.DAOLoggingSupport; -import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; -import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ResourceNotFoundException; -import ch.ethz.seb.sebserver.webservice.servicelayer.dao.TransactionHandler; -import io.micrometer.core.instrument.util.StringUtils; - -@Lazy -@Component -@WebServiceProfile -public class ClientConnectionDAOImpl implements ClientConnectionDAO { - - private final ClientConnectionRecordMapper clientConnectionRecordMapper; - private final ClientEventRecordMapper clientEventRecordMapper; - - protected ClientConnectionDAOImpl( - final ClientConnectionRecordMapper clientConnectionRecordMapper, - final ClientEventRecordMapper clientEventRecordMapper) { - - this.clientConnectionRecordMapper = clientConnectionRecordMapper; - this.clientEventRecordMapper = clientEventRecordMapper; - } - - @Override - public EntityType entityType() { - return EntityType.CLIENT_CONNECTION; - } - - @Override - @Transactional(readOnly = true) - public Result byPK(final Long id) { - return recordById(id) - .flatMap(ClientConnectionDAOImpl::toDomainModel); - } - - @Override - @Transactional(readOnly = true) - public Result> allMatching( - final FilterMap filterMap, - final Predicate predicate) { - - return Result.tryCatch(() -> this.clientConnectionRecordMapper - .selectByExample() - .where( - ClientConnectionRecordDynamicSqlSupport.institutionId, - isEqualToWhenPresent(filterMap.getInstitutionId())) - .and( - ClientConnectionRecordDynamicSqlSupport.examId, - isEqualToWhenPresent(filterMap.getClientConnectionExamId())) - .and( - ClientConnectionRecordDynamicSqlSupport.status, - isEqualToWhenPresent(filterMap.getClientConnectionStatus())) - .build() - .execute() - .stream() - .map(ClientConnectionDAOImpl::toDomainModel) - .flatMap(DAOLoggingSupport::logAndSkipOnError) - .filter(predicate) - .collect(Collectors.toList())); - } - - @Override - @Transactional(readOnly = true) - public Result> allOf(final Set pks) { - return Result.tryCatch(() -> { - return this.clientConnectionRecordMapper.selectByExample() - .where(ClientConnectionRecordDynamicSqlSupport.id, isIn(new ArrayList<>(pks))) - .build() - .execute() - .stream() - .map(ClientConnectionDAOImpl::toDomainModel) - .flatMap(DAOLoggingSupport::logAndSkipOnError) - .collect(Collectors.toList()); - }); - } - - @Override - @Transactional(readOnly = true) - public Result> getConnectionTokens(final Long examId) { - return Result.tryCatch(() -> { - return this.clientConnectionRecordMapper - .selectByExample() - .where( - ClientConnectionRecordDynamicSqlSupport.examId, - SqlBuilder.isEqualTo(examId)) - .build() - .execute() - .stream() - .map(ClientConnectionRecord::getConnectionToken) - .filter(StringUtils::isNotBlank) - .collect(Collectors.toList()); - }); - } - - @Override - @Transactional - public Result createNew(final ClientConnection data) { - return Result.tryCatch(() -> { - - final ClientConnectionRecord newRecord = new ClientConnectionRecord( - null, - data.institutionId, - data.examId, - ConnectionStatus.CONNECTION_REQUESTED.name(), - data.connectionToken, - null, - data.clientAddress, - data.virtualClientAddress, - Utils.getMillisecondsNow()); - - this.clientConnectionRecordMapper.insert(newRecord); - return newRecord; - }) - .flatMap(ClientConnectionDAOImpl::toDomainModel) - .onError(TransactionHandler::rollback); - } - - @Override - @Transactional - public Result save(final ClientConnection data) { - return Result.tryCatch(() -> { - - final ClientConnectionRecord updateRecord = new ClientConnectionRecord( - data.id, - null, - data.examId, - data.status != null ? data.status.name() : null, - null, - data.userSessionId, - data.clientAddress, - data.virtualClientAddress, - null); - - this.clientConnectionRecordMapper.updateByPrimaryKeySelective(updateRecord); - return this.clientConnectionRecordMapper.selectByPrimaryKey(data.id); - }) - .flatMap(ClientConnectionDAOImpl::toDomainModel) - .onError(TransactionHandler::rollback); - } - - @Override - @Transactional - public Result> delete(final Set all) { - return Result.tryCatch(() -> { - - final List ids = extractListOfPKs(all); - - // first delete all related client events - this.clientEventRecordMapper.deleteByExample() - .where( - ClientEventRecordDynamicSqlSupport.clientConnectionId, - SqlBuilder.isIn(ids)) - .build() - .execute(); - - // then delete all requested client-connections - this.clientConnectionRecordMapper.deleteByExample() - .where( - ClientConnectionRecordDynamicSqlSupport.id, - SqlBuilder.isIn(ids)) - .build() - .execute(); - - return ids.stream() - .map(id -> new EntityKey(id, EntityType.CLIENT_CONNECTION)) - .collect(Collectors.toList()); - }); - } - - @Override - public Result byConnectionToken(final String connectionToken) { - return Result.tryCatch(() -> { - final List list = this.clientConnectionRecordMapper - .selectByExample() - .where( - ClientConnectionRecordDynamicSqlSupport.connectionToken, - SqlBuilder.isEqualTo(connectionToken)) - - .build() - .execute(); - - if (list.isEmpty()) { - throw new ResourceNotFoundException(EntityType.CLIENT_CONNECTION, "connectionToken"); - } - - if (list.size() > 1) { - throw new IllegalStateException("Only one ClientConnection expected but there are: " + list.size()); - } - - return list.get(0); - }) - .flatMap(ClientConnectionDAOImpl::toDomainModel); - } - - private Result recordById(final Long id) { - return Result.tryCatch(() -> { - - final ClientConnectionRecord record = this.clientConnectionRecordMapper.selectByPrimaryKey(id); - if (record == null) { - throw new ResourceNotFoundException( - entityType(), - String.valueOf(id)); - } - - return record; - }); - } - - private static Result toDomainModel(final ClientConnectionRecord record) { - return Result.tryCatch(() -> { - - final String status = record.getStatus(); - return new ClientConnection( - record.getId(), - record.getInstitutionId(), - record.getExamId(), - (StringUtils.isNotBlank(status)) - ? ConnectionStatus.valueOf(status) - : ConnectionStatus.UNDEFINED, - record.getConnectionToken(), - record.getExamUserSessionId(), - record.getClientAddress(), - record.getVirtualClientAddress(), - record.getCreationTime()); - }); - - } - -} +/* + * 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.dao.impl; + +import static org.mybatis.dynamic.sql.SqlBuilder.isEqualToWhenPresent; +import static org.mybatis.dynamic.sql.SqlBuilder.isIn; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import org.mybatis.dynamic.sql.SqlBuilder; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import ch.ethz.seb.sebserver.gbl.api.EntityType; +import ch.ethz.seb.sebserver.gbl.model.EntityKey; +import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; +import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus; +import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; +import ch.ethz.seb.sebserver.gbl.util.Result; +import ch.ethz.seb.sebserver.gbl.util.Utils; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientConnectionRecordDynamicSqlSupport; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientConnectionRecordMapper; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientEventRecordDynamicSqlSupport; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientEventRecordMapper; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientConnectionRecord; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientConnectionDAO; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.DAOLoggingSupport; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ResourceNotFoundException; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.TransactionHandler; +import io.micrometer.core.instrument.util.StringUtils; + +@Lazy +@Component +@WebServiceProfile +public class ClientConnectionDAOImpl implements ClientConnectionDAO { + + private final ClientConnectionRecordMapper clientConnectionRecordMapper; + private final ClientEventRecordMapper clientEventRecordMapper; + + protected ClientConnectionDAOImpl( + final ClientConnectionRecordMapper clientConnectionRecordMapper, + final ClientEventRecordMapper clientEventRecordMapper) { + + this.clientConnectionRecordMapper = clientConnectionRecordMapper; + this.clientEventRecordMapper = clientEventRecordMapper; + } + + @Override + public EntityType entityType() { + return EntityType.CLIENT_CONNECTION; + } + + @Override + @Transactional(readOnly = true) + public Result byPK(final Long id) { + return recordById(id) + .flatMap(ClientConnectionDAOImpl::toDomainModel); + } + + @Override + @Transactional(readOnly = true) + public Result> allMatching( + final FilterMap filterMap, + final Predicate predicate) { + + return Result.tryCatch(() -> this.clientConnectionRecordMapper + .selectByExample() + .where( + ClientConnectionRecordDynamicSqlSupport.institutionId, + isEqualToWhenPresent(filterMap.getInstitutionId())) + .and( + ClientConnectionRecordDynamicSqlSupport.examId, + isEqualToWhenPresent(filterMap.getClientConnectionExamId())) + .and( + ClientConnectionRecordDynamicSqlSupport.status, + isEqualToWhenPresent(filterMap.getClientConnectionStatus())) + .build() + .execute() + .stream() + .map(ClientConnectionDAOImpl::toDomainModel) + .flatMap(DAOLoggingSupport::logAndSkipOnError) + .filter(predicate) + .collect(Collectors.toList())); + } + + @Override + @Transactional(readOnly = true) + public Result> allOf(final Set pks) { + return Result.tryCatch(() -> this.clientConnectionRecordMapper.selectByExample() + .where(ClientConnectionRecordDynamicSqlSupport.id, isIn(new ArrayList<>(pks))) + .build() + .execute() + .stream() + .map(ClientConnectionDAOImpl::toDomainModel) + .flatMap(DAOLoggingSupport::logAndSkipOnError) + .collect(Collectors.toList())); + } + + @Override + @Transactional(readOnly = true) + public Result> getConnectionTokens(final Long examId) { + return Result.tryCatch(() -> { + return this.clientConnectionRecordMapper + .selectByExample() + .where( + ClientConnectionRecordDynamicSqlSupport.examId, + SqlBuilder.isEqualTo(examId)) + .build() + .execute() + .stream() + .map(ClientConnectionRecord::getConnectionToken) + .filter(StringUtils::isNotBlank) + .collect(Collectors.toList()); + }); + } + + @Override + @Transactional + public Result createNew(final ClientConnection data) { + return Result.tryCatch(() -> { + + final ClientConnectionRecord newRecord = new ClientConnectionRecord( + null, + data.institutionId, + data.examId, + ConnectionStatus.CONNECTION_REQUESTED.name(), + data.connectionToken, + null, + data.clientAddress, + data.virtualClientAddress, + Utils.getMillisecondsNow()); + + this.clientConnectionRecordMapper.insert(newRecord); + return newRecord; + }) + .flatMap(ClientConnectionDAOImpl::toDomainModel) + .onError(TransactionHandler::rollback); + } + + @Override + @Transactional + public Result save(final ClientConnection data) { + return Result.tryCatch(() -> { + + final ClientConnectionRecord updateRecord = new ClientConnectionRecord( + data.id, + null, + data.examId, + data.status != null ? data.status.name() : null, + null, + data.userSessionId, + data.clientAddress, + data.virtualClientAddress, + null); + + this.clientConnectionRecordMapper.updateByPrimaryKeySelective(updateRecord); + return this.clientConnectionRecordMapper.selectByPrimaryKey(data.id); + }) + .flatMap(ClientConnectionDAOImpl::toDomainModel) + .onError(TransactionHandler::rollback); + } + + @Override + @Transactional + public Result> delete(final Set all) { + return Result.tryCatch(() -> { + + final List ids = extractListOfPKs(all); + + // first delete all related client events + this.clientEventRecordMapper.deleteByExample() + .where( + ClientEventRecordDynamicSqlSupport.clientConnectionId, + SqlBuilder.isIn(ids)) + .build() + .execute(); + + // then delete all requested client-connections + this.clientConnectionRecordMapper.deleteByExample() + .where( + ClientConnectionRecordDynamicSqlSupport.id, + SqlBuilder.isIn(ids)) + .build() + .execute(); + + return ids.stream() + .map(id -> new EntityKey(id, EntityType.CLIENT_CONNECTION)) + .collect(Collectors.toList()); + }); + } + + @Override + public Result byConnectionToken(final String connectionToken) { + return Result.tryCatch(() -> { + final List list = this.clientConnectionRecordMapper + .selectByExample() + .where( + ClientConnectionRecordDynamicSqlSupport.connectionToken, + SqlBuilder.isEqualTo(connectionToken)) + + .build() + .execute(); + + if (list.isEmpty()) { + throw new ResourceNotFoundException(EntityType.CLIENT_CONNECTION, "connectionToken"); + } + + if (list.size() > 1) { + throw new IllegalStateException("Only one ClientConnection expected but there are: " + list.size()); + } + + return list.get(0); + }) + .flatMap(ClientConnectionDAOImpl::toDomainModel); + } + + private Result recordById(final Long id) { + return Result.tryCatch(() -> { + + final ClientConnectionRecord record = this.clientConnectionRecordMapper.selectByPrimaryKey(id); + if (record == null) { + throw new ResourceNotFoundException( + entityType(), + String.valueOf(id)); + } + + return record; + }); + } + + private static Result toDomainModel(final ClientConnectionRecord record) { + return Result.tryCatch(() -> { + + final String status = record.getStatus(); + return new ClientConnection( + record.getId(), + record.getInstitutionId(), + record.getExamId(), + (StringUtils.isNotBlank(status)) + ? ConnectionStatus.valueOf(status) + : ConnectionStatus.UNDEFINED, + record.getConnectionToken(), + record.getExamUserSessionId(), + record.getClientAddress(), + record.getVirtualClientAddress(), + record.getCreationTime()); + }); + + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ClientEventDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ClientEventDAOImpl.java index 683dc114..562620dc 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ClientEventDAOImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ClientEventDAOImpl.java @@ -1,265 +1,257 @@ -/* - * 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.dao.impl; - -import static org.mybatis.dynamic.sql.SqlBuilder.isEqualToWhenPresent; -import static org.mybatis.dynamic.sql.SqlBuilder.isIn; - -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Set; -import java.util.function.Predicate; -import java.util.stream.Collectors; - -import org.mybatis.dynamic.sql.SqlBuilder; -import org.springframework.context.annotation.Lazy; -import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; - -import ch.ethz.seb.sebserver.gbl.api.EntityType; -import ch.ethz.seb.sebserver.gbl.model.EntityKey; -import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; -import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent; -import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.EventType; -import ch.ethz.seb.sebserver.gbl.model.session.ExtendedClientEvent; -import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; -import ch.ethz.seb.sebserver.gbl.util.Result; -import ch.ethz.seb.sebserver.webservice.datalayer.batis.ClientEventExtentionMapper; -import ch.ethz.seb.sebserver.webservice.datalayer.batis.ClientEventExtentionMapper.ConnectionEventJoinRecord; -import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientConnectionRecordDynamicSqlSupport; -import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientEventRecordDynamicSqlSupport; -import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientEventRecordMapper; -import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientEventRecord; -import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientEventDAO; -import ch.ethz.seb.sebserver.webservice.servicelayer.dao.DAOLoggingSupport; -import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; -import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ResourceNotFoundException; -import ch.ethz.seb.sebserver.webservice.servicelayer.dao.TransactionHandler; - -@Lazy -@Component -@WebServiceProfile -public class ClientEventDAOImpl implements ClientEventDAO { - - private final ClientEventRecordMapper clientEventRecordMapper; - private final ClientEventExtentionMapper clientEventExtentionMapper; - - protected ClientEventDAOImpl( - final ClientEventRecordMapper clientEventRecordMapper, - final ClientEventExtentionMapper clientEventExtentionMapper) { - - this.clientEventRecordMapper = clientEventRecordMapper; - this.clientEventExtentionMapper = clientEventExtentionMapper; - } - - @Override - public EntityType entityType() { - return EntityType.CLIENT_EVENT; - } - - @Override - @Transactional(readOnly = true) - public Result byPK(final Long id) { - return recordById(id) - .flatMap(ClientEventDAOImpl::toDomainModel); - } - - @Override - @Transactional(readOnly = true) - public Result> allMatching( - final FilterMap filterMap, - final Predicate predicate) { - - return Result.tryCatch(() -> { - - return this.clientEventRecordMapper - .selectByExample() - .where( - ClientEventRecordDynamicSqlSupport.clientConnectionId, - isEqualToWhenPresent(filterMap.getClientEventConnectionId())) - .and( - ClientEventRecordDynamicSqlSupport.type, - isEqualToWhenPresent(filterMap.getClientEventTypeId())) - .and( - ClientEventRecordDynamicSqlSupport.type, - SqlBuilder.isNotEqualTo(EventType.LAST_PING.id)) - .and( - ClientEventRecordDynamicSqlSupport.clientTime, - SqlBuilder.isGreaterThanOrEqualToWhenPresent(filterMap.getClientEventClientTimeFrom())) - .and( - ClientEventRecordDynamicSqlSupport.clientTime, - SqlBuilder.isLessThanOrEqualToWhenPresent(filterMap.getClientEventClientTimeTo())) - .and( - ClientEventRecordDynamicSqlSupport.serverTime, - SqlBuilder.isGreaterThanOrEqualToWhenPresent(filterMap.getClientEventServerTimeFrom())) - .and( - ClientEventRecordDynamicSqlSupport.serverTime, - SqlBuilder.isLessThanOrEqualToWhenPresent(filterMap.getClientEventServerTimeTo())) - .and( - ClientEventRecordDynamicSqlSupport.text, - SqlBuilder.isLikeWhenPresent(filterMap.getClientEventText())) - .build() - .execute() - .stream() - .map(ClientEventDAOImpl::toDomainModel) - .flatMap(DAOLoggingSupport::logAndSkipOnError) - .filter(predicate) - .collect(Collectors.toList()); - }); - } - - @Override - public Result> allMatchingExtended( - final FilterMap filterMap, - final Predicate predicate) { - - return Result.tryCatch(() -> this.clientEventExtentionMapper.selectByExample() - .where( - ClientConnectionRecordDynamicSqlSupport.institutionId, - isEqualToWhenPresent(filterMap.getInstitutionId())) - .and( - ClientConnectionRecordDynamicSqlSupport.examId, - isEqualToWhenPresent(filterMap.getClientEventExamId())) - .and( - ClientConnectionRecordDynamicSqlSupport.examUserSessionId, - SqlBuilder.isLikeWhenPresent(filterMap.getSQLWildcard(ClientConnection.FILTER_ATTR_SESSION_ID))) - .and( - ClientEventRecordDynamicSqlSupport.clientConnectionId, - isEqualToWhenPresent(filterMap.getClientEventConnectionId())) - .and( - ClientEventRecordDynamicSqlSupport.type, - isEqualToWhenPresent(filterMap.getClientEventTypeId())) - .and( - ClientEventRecordDynamicSqlSupport.type, - SqlBuilder.isNotEqualTo(EventType.LAST_PING.id)) - .and( - ClientEventRecordDynamicSqlSupport.clientTime, - SqlBuilder.isGreaterThanOrEqualToWhenPresent(filterMap.getClientEventClientTimeFrom())) - .and( - ClientEventRecordDynamicSqlSupport.clientTime, - SqlBuilder.isLessThanOrEqualToWhenPresent(filterMap.getClientEventClientTimeTo())) - .and( - ClientEventRecordDynamicSqlSupport.serverTime, - SqlBuilder.isGreaterThanOrEqualToWhenPresent(filterMap.getClientEventServerTimeFrom())) - .and( - ClientEventRecordDynamicSqlSupport.serverTime, - SqlBuilder.isLessThanOrEqualToWhenPresent(filterMap.getClientEventServerTimeTo())) - .and( - ClientEventRecordDynamicSqlSupport.text, - SqlBuilder.isLikeWhenPresent(filterMap.getClientEventText())) - .build() - .execute() - .stream() - .map(ClientEventDAOImpl::toDomainModelExtended) - .flatMap(DAOLoggingSupport::logAndSkipOnError) - .filter(predicate) - .collect(Collectors.toList())); - } - - @Override - @Transactional(readOnly = true) - public Result> allOf(final Set pks) { - return Result.tryCatch(() -> { - return this.clientEventRecordMapper.selectByExample() - .where(ClientEventRecordDynamicSqlSupport.id, isIn(new ArrayList<>(pks))) - .build() - .execute() - .stream() - .map(ClientEventDAOImpl::toDomainModel) - .flatMap(DAOLoggingSupport::logAndSkipOnError) - .collect(Collectors.toList()); - }); - } - - @Override - @Transactional - public Result createNew(final ClientEvent data) { - return Result.tryCatch(() -> { - - final EventType eventType = data.getEventType(); - - final ClientEventRecord newRecord = new ClientEventRecord( - null, - data.connectionId, - (eventType != null) ? eventType.id : EventType.UNKNOWN.id, - data.clientTime, - data.serverTime, - (data.numValue != null) ? new BigDecimal(data.numValue) : null, - data.text); - - this.clientEventRecordMapper.insertSelective(newRecord); - return newRecord; - }) - .flatMap(ClientEventDAOImpl::toDomainModel) - .onError(TransactionHandler::rollback); - } - - @Override - @Transactional - public Result save(final ClientEvent data) { - throw new UnsupportedOperationException("Update is not supported for client events"); - } - - @Override - @Transactional - public Result> delete(final Set all) { - throw new UnsupportedOperationException( - "Delete is not supported for particular client events. " - + "Use delete of a client connection to delete also all client events of this connection."); - } - - private Result recordById(final Long id) { - return Result.tryCatch(() -> { - - final ClientEventRecord record = this.clientEventRecordMapper.selectByPrimaryKey(id); - if (record == null) { - throw new ResourceNotFoundException( - entityType(), - String.valueOf(id)); - } - - return record; - }); - } - - private static Result toDomainModel(final ClientEventRecord record) { - return Result.tryCatch(() -> { - - final Integer type = record.getType(); - final BigDecimal numericValue = record.getNumericValue(); - return new ClientEvent( - record.getId(), - record.getClientConnectionId(), - (type != null) ? EventType.byId(type) : EventType.UNKNOWN, - record.getClientTime(), - record.getServerTime(), - (numericValue != null) ? numericValue.doubleValue() : null, - record.getText()); - }); - } - - private static Result toDomainModelExtended(final ConnectionEventJoinRecord record) { - return Result.tryCatch(() -> { - - return new ExtendedClientEvent( - record.institution_id, - record.exam_id, - record.exam_user_session_identifer, - record.id, - record.connection_id, - (record.type != null) ? EventType.byId(record.type) : EventType.UNKNOWN, - record.client_time, - record.server_time, - (record.numeric_value != null) ? record.numeric_value.doubleValue() : null, - record.text); - }); - } - -} +/* + * 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.dao.impl; + +import static org.mybatis.dynamic.sql.SqlBuilder.isEqualToWhenPresent; +import static org.mybatis.dynamic.sql.SqlBuilder.isIn; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import org.mybatis.dynamic.sql.SqlBuilder; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import ch.ethz.seb.sebserver.gbl.api.EntityType; +import ch.ethz.seb.sebserver.gbl.model.EntityKey; +import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; +import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent; +import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.EventType; +import ch.ethz.seb.sebserver.gbl.model.session.ExtendedClientEvent; +import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; +import ch.ethz.seb.sebserver.gbl.util.Result; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.ClientEventExtentionMapper; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.ClientEventExtentionMapper.ConnectionEventJoinRecord; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientConnectionRecordDynamicSqlSupport; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientEventRecordDynamicSqlSupport; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientEventRecordMapper; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientEventRecord; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientEventDAO; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.DAOLoggingSupport; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ResourceNotFoundException; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.TransactionHandler; + +@Lazy +@Component +@WebServiceProfile +public class ClientEventDAOImpl implements ClientEventDAO { + + private final ClientEventRecordMapper clientEventRecordMapper; + private final ClientEventExtentionMapper clientEventExtentionMapper; + + protected ClientEventDAOImpl( + final ClientEventRecordMapper clientEventRecordMapper, + final ClientEventExtentionMapper clientEventExtentionMapper) { + + this.clientEventRecordMapper = clientEventRecordMapper; + this.clientEventExtentionMapper = clientEventExtentionMapper; + } + + @Override + public EntityType entityType() { + return EntityType.CLIENT_EVENT; + } + + @Override + @Transactional(readOnly = true) + public Result byPK(final Long id) { + return recordById(id) + .flatMap(ClientEventDAOImpl::toDomainModel); + } + + @Override + @Transactional(readOnly = true) + public Result> allMatching( + final FilterMap filterMap, + final Predicate predicate) { + + return Result.tryCatch(() -> this.clientEventRecordMapper + .selectByExample() + .where( + ClientEventRecordDynamicSqlSupport.clientConnectionId, + isEqualToWhenPresent(filterMap.getClientEventConnectionId())) + .and( + ClientEventRecordDynamicSqlSupport.type, + isEqualToWhenPresent(filterMap.getClientEventTypeId())) + .and( + ClientEventRecordDynamicSqlSupport.type, + SqlBuilder.isNotEqualTo(EventType.LAST_PING.id)) + .and( + ClientEventRecordDynamicSqlSupport.clientTime, + SqlBuilder.isGreaterThanOrEqualToWhenPresent(filterMap.getClientEventClientTimeFrom())) + .and( + ClientEventRecordDynamicSqlSupport.clientTime, + SqlBuilder.isLessThanOrEqualToWhenPresent(filterMap.getClientEventClientTimeTo())) + .and( + ClientEventRecordDynamicSqlSupport.serverTime, + SqlBuilder.isGreaterThanOrEqualToWhenPresent(filterMap.getClientEventServerTimeFrom())) + .and( + ClientEventRecordDynamicSqlSupport.serverTime, + SqlBuilder.isLessThanOrEqualToWhenPresent(filterMap.getClientEventServerTimeTo())) + .and( + ClientEventRecordDynamicSqlSupport.text, + SqlBuilder.isLikeWhenPresent(filterMap.getClientEventText())) + .build() + .execute() + .stream() + .map(ClientEventDAOImpl::toDomainModel) + .flatMap(DAOLoggingSupport::logAndSkipOnError) + .filter(predicate) + .collect(Collectors.toList())); + } + + @Override + public Result> allMatchingExtended( + final FilterMap filterMap, + final Predicate predicate) { + + return Result.tryCatch(() -> this.clientEventExtentionMapper.selectByExample() + .where( + ClientConnectionRecordDynamicSqlSupport.institutionId, + isEqualToWhenPresent(filterMap.getInstitutionId())) + .and( + ClientConnectionRecordDynamicSqlSupport.examId, + isEqualToWhenPresent(filterMap.getClientEventExamId())) + .and( + ClientConnectionRecordDynamicSqlSupport.examUserSessionId, + SqlBuilder.isLikeWhenPresent(filterMap.getSQLWildcard(ClientConnection.FILTER_ATTR_SESSION_ID))) + .and( + ClientEventRecordDynamicSqlSupport.clientConnectionId, + isEqualToWhenPresent(filterMap.getClientEventConnectionId())) + .and( + ClientEventRecordDynamicSqlSupport.type, + isEqualToWhenPresent(filterMap.getClientEventTypeId())) + .and( + ClientEventRecordDynamicSqlSupport.type, + SqlBuilder.isNotEqualTo(EventType.LAST_PING.id)) + .and( + ClientEventRecordDynamicSqlSupport.clientTime, + SqlBuilder.isGreaterThanOrEqualToWhenPresent(filterMap.getClientEventClientTimeFrom())) + .and( + ClientEventRecordDynamicSqlSupport.clientTime, + SqlBuilder.isLessThanOrEqualToWhenPresent(filterMap.getClientEventClientTimeTo())) + .and( + ClientEventRecordDynamicSqlSupport.serverTime, + SqlBuilder.isGreaterThanOrEqualToWhenPresent(filterMap.getClientEventServerTimeFrom())) + .and( + ClientEventRecordDynamicSqlSupport.serverTime, + SqlBuilder.isLessThanOrEqualToWhenPresent(filterMap.getClientEventServerTimeTo())) + .and( + ClientEventRecordDynamicSqlSupport.text, + SqlBuilder.isLikeWhenPresent(filterMap.getClientEventText())) + .build() + .execute() + .stream() + .map(ClientEventDAOImpl::toDomainModelExtended) + .flatMap(DAOLoggingSupport::logAndSkipOnError) + .filter(predicate) + .collect(Collectors.toList())); + } + + @Override + @Transactional(readOnly = true) + public Result> allOf(final Set pks) { + return Result.tryCatch(() -> this.clientEventRecordMapper.selectByExample() + .where(ClientEventRecordDynamicSqlSupport.id, isIn(new ArrayList<>(pks))) + .build() + .execute() + .stream() + .map(ClientEventDAOImpl::toDomainModel) + .flatMap(DAOLoggingSupport::logAndSkipOnError) + .collect(Collectors.toList())); + } + + @Override + @Transactional + public Result createNew(final ClientEvent data) { + return Result.tryCatch(() -> { + + final EventType eventType = data.getEventType(); + + final ClientEventRecord newRecord = new ClientEventRecord( + null, + data.connectionId, + (eventType != null) ? eventType.id : EventType.UNKNOWN.id, + data.clientTime, + data.serverTime, + (data.numValue != null) ? new BigDecimal(data.numValue) : null, + data.text); + + this.clientEventRecordMapper.insertSelective(newRecord); + return newRecord; + }) + .flatMap(ClientEventDAOImpl::toDomainModel) + .onError(TransactionHandler::rollback); + } + + @Override + @Transactional + public Result save(final ClientEvent data) { + throw new UnsupportedOperationException("Update is not supported for client events"); + } + + @Override + @Transactional + public Result> delete(final Set all) { + throw new UnsupportedOperationException( + "Delete is not supported for particular client events. " + + "Use delete of a client connection to delete also all client events of this connection."); + } + + private Result recordById(final Long id) { + return Result.tryCatch(() -> { + + final ClientEventRecord record = this.clientEventRecordMapper.selectByPrimaryKey(id); + if (record == null) { + throw new ResourceNotFoundException( + entityType(), + String.valueOf(id)); + } + + return record; + }); + } + + private static Result toDomainModel(final ClientEventRecord record) { + return Result.tryCatch(() -> { + + final Integer type = record.getType(); + final BigDecimal numericValue = record.getNumericValue(); + return new ClientEvent( + record.getId(), + record.getClientConnectionId(), + (type != null) ? EventType.byId(type) : EventType.UNKNOWN, + record.getClientTime(), + record.getServerTime(), + (numericValue != null) ? numericValue.doubleValue() : null, + record.getText()); + }); + } + + private static Result toDomainModelExtended(final ConnectionEventJoinRecord record) { + return Result.tryCatch(() -> new ExtendedClientEvent( + record.institution_id, + record.exam_id, + record.exam_user_session_identifer, + record.id, + record.connection_id, + (record.type != null) ? EventType.byId(record.type) : EventType.UNKNOWN, + record.client_time, + record.server_time, + record.numeric_value, + record.text)); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ConfigurationAttributeDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ConfigurationAttributeDAOImpl.java index 4e3f180e..ea907528 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ConfigurationAttributeDAOImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ConfigurationAttributeDAOImpl.java @@ -1,266 +1,264 @@ -/* - * 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.dao.impl; - -import static org.mybatis.dynamic.sql.SqlBuilder.isIn; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Set; -import java.util.function.Predicate; -import java.util.stream.Collectors; - -import org.mybatis.dynamic.sql.SqlBuilder; -import org.springframework.context.annotation.Lazy; -import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; - -import ch.ethz.seb.sebserver.gbl.api.EntityType; -import ch.ethz.seb.sebserver.gbl.model.EntityKey; -import ch.ethz.seb.sebserver.gbl.model.sebconfig.AttributeType; -import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationAttribute; -import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; -import ch.ethz.seb.sebserver.gbl.util.Result; -import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ConfigurationAttributeRecordDynamicSqlSupport; -import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ConfigurationAttributeRecordMapper; -import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ConfigurationValueRecordDynamicSqlSupport; -import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ConfigurationValueRecordMapper; -import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.OrientationRecordDynamicSqlSupport; -import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.OrientationRecordMapper; -import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ConfigurationAttributeRecord; -import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationAttributeDAO; -import ch.ethz.seb.sebserver.webservice.servicelayer.dao.DAOLoggingSupport; -import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; -import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ResourceNotFoundException; -import ch.ethz.seb.sebserver.webservice.servicelayer.dao.TransactionHandler; - -@Lazy -@Component -@WebServiceProfile -public class ConfigurationAttributeDAOImpl implements ConfigurationAttributeDAO { - - private final ConfigurationAttributeRecordMapper configurationAttributeRecordMapper; - private final ConfigurationValueRecordMapper configurationValueRecordMapper; - private final OrientationRecordMapper orientationRecordMapper; - - protected ConfigurationAttributeDAOImpl( - final ConfigurationAttributeRecordMapper configurationAttributeRecordMapper, - final ConfigurationValueRecordMapper configurationValueRecordMapper, - final OrientationRecordMapper orientationRecordMapper) { - - this.configurationAttributeRecordMapper = configurationAttributeRecordMapper; - this.configurationValueRecordMapper = configurationValueRecordMapper; - this.orientationRecordMapper = orientationRecordMapper; - } - - @Override - public EntityType entityType() { - return EntityType.CONFIGURATION_ATTRIBUTE; - } - - @Override - @Transactional(readOnly = true) - public Result byPK(final Long id) { - return recordById(id) - .flatMap(ConfigurationAttributeDAOImpl::toDomainModel); - } - - @Override - @Transactional(readOnly = true) - public Result> allOf(final Set pks) { - return Result.tryCatch(() -> { - return this.configurationAttributeRecordMapper.selectByExample() - .where(ConfigurationAttributeRecordDynamicSqlSupport.id, isIn(new ArrayList<>(pks))) - .build() - .execute() - .stream() - .map(ConfigurationAttributeDAOImpl::toDomainModel) - .flatMap(DAOLoggingSupport::logAndSkipOnError) - .collect(Collectors.toList()); - }); - } - - @Override - @Transactional(readOnly = true) - public Result> allMatching( - final FilterMap filterMap, - final Predicate predicate) { - - return Result.tryCatch(() -> this.configurationAttributeRecordMapper - .selectByExample() - .where( - ConfigurationAttributeRecordDynamicSqlSupport.parentId, - SqlBuilder.isEqualToWhenPresent(filterMap.getConfigAttributeParentId())) - .and( - ConfigurationAttributeRecordDynamicSqlSupport.type, - SqlBuilder.isEqualToWhenPresent(filterMap.getConfigAttributeType())) - .build() - .execute() - .stream() - .map(ConfigurationAttributeDAOImpl::toDomainModel) - .flatMap(DAOLoggingSupport::logAndSkipOnError) - .filter(predicate) - .collect(Collectors.toList())); - } - - @Override - @Transactional(readOnly = true) - public Result> allChildAttributes(final Long parentId) { - return Result.tryCatch(() -> this.configurationAttributeRecordMapper - .selectByExample() - .where( - ConfigurationAttributeRecordDynamicSqlSupport.parentId, - SqlBuilder.isEqualTo(parentId)) - .build() - .execute() - .stream() - .map(ConfigurationAttributeDAOImpl::toDomainModel) - .flatMap(DAOLoggingSupport::logAndSkipOnError) - .collect(Collectors.toList())); - } - - @Override - @Transactional(readOnly = true) - public Result> getAllRootAttributes() { - return Result.tryCatch(() -> this.configurationAttributeRecordMapper - .selectByExample() - .where( - ConfigurationAttributeRecordDynamicSqlSupport.parentId, - SqlBuilder.isNull()) - .build() - .execute() - .stream() - .map(ConfigurationAttributeDAOImpl::toDomainModel) - .flatMap(DAOLoggingSupport::logAndSkipOnError) - .collect(Collectors.toList())); - } - - @Override - @Transactional - public Result createNew(final ConfigurationAttribute data) { - return Result.tryCatch(() -> { - final ConfigurationAttributeRecord newRecord = new ConfigurationAttributeRecord( - null, - data.name, - data.type.name(), - data.parentId, - data.resources, - data.validator, - data.dependencies, - data.defaultValue); - - this.configurationAttributeRecordMapper.insert(newRecord); - return newRecord; - }) - .flatMap(ConfigurationAttributeDAOImpl::toDomainModel) - .onError(TransactionHandler::rollback); - } - - @Override - @Transactional - public Result save(final ConfigurationAttribute data) { - return Result.tryCatch(() -> { - - final ConfigurationAttributeRecord newRecord = new ConfigurationAttributeRecord( - data.id, - data.name, - data.type.name(), - data.parentId, - data.resources, - data.validator, - data.dependencies, - data.defaultValue); - - this.configurationAttributeRecordMapper.updateByPrimaryKeySelective(newRecord); - return this.configurationAttributeRecordMapper.selectByPrimaryKey(data.id); - }) - .flatMap(ConfigurationAttributeDAOImpl::toDomainModel) - .onError(TransactionHandler::rollback); - } - - @Override - @Transactional - public Result> delete(final Set all) { - return Result.tryCatch(() -> { - - final List ids = extractListOfPKs(all); - final List result = new ArrayList<>(); - - // if this is a complex attribute that has children, delete the children first - final List children = - this.configurationAttributeRecordMapper.selectByExample() - .where(ConfigurationAttributeRecordDynamicSqlSupport.parentId, isIn(ids)) - .build() - .execute(); - - // recursive call for children and adding result to the overall result set - if (children != null && !children.isEmpty()) { - result.addAll(delete(children.stream() - .map(r -> new EntityKey(r.getId(), EntityType.CONFIGURATION_ATTRIBUTE)) - .collect(Collectors.toSet())) - .getOrThrow()); - } - - // delete all ConfigurationValue's that belongs to the ConfigurationAttributes to delete - this.configurationValueRecordMapper.deleteByExample() - .where( - ConfigurationValueRecordDynamicSqlSupport.configurationAttributeId, - SqlBuilder.isIn(ids)) - .build() - .execute(); - - // delete all Orientations that belongs to the ConfigurationAttributes to delete - this.orientationRecordMapper.deleteByExample() - .where( - OrientationRecordDynamicSqlSupport.configAttributeId, - SqlBuilder.isIn(ids)) - .build() - .execute(); - - // then delete all requested ConfigurationAttributes - this.configurationAttributeRecordMapper.deleteByExample() - .where(ConfigurationAttributeRecordDynamicSqlSupport.id, isIn(ids)) - .build() - .execute(); - - result.addAll(ids.stream() - .map(id -> new EntityKey(id, EntityType.CONFIGURATION_ATTRIBUTE)) - .collect(Collectors.toList())); - - return result; - }); - } - - Result recordById(final Long id) { - return Result.tryCatch(() -> { - final ConfigurationAttributeRecord record = this.configurationAttributeRecordMapper - .selectByPrimaryKey(id); - if (record == null) { - throw new ResourceNotFoundException( - EntityType.CONFIGURATION_ATTRIBUTE, - String.valueOf(id)); - } - return record; - }); - } - - static Result toDomainModel(final ConfigurationAttributeRecord record) { - return Result.tryCatch(() -> new ConfigurationAttribute( - record.getId(), - record.getParentId(), - record.getName(), - AttributeType.valueOf(record.getType()), - record.getResources(), - record.getValidator(), - record.getDependencies(), - record.getDefaultValue())); - } - -} +/* + * 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.dao.impl; + +import static org.mybatis.dynamic.sql.SqlBuilder.isIn; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import org.mybatis.dynamic.sql.SqlBuilder; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import ch.ethz.seb.sebserver.gbl.api.EntityType; +import ch.ethz.seb.sebserver.gbl.model.EntityKey; +import ch.ethz.seb.sebserver.gbl.model.sebconfig.AttributeType; +import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationAttribute; +import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; +import ch.ethz.seb.sebserver.gbl.util.Result; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ConfigurationAttributeRecordDynamicSqlSupport; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ConfigurationAttributeRecordMapper; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ConfigurationValueRecordDynamicSqlSupport; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ConfigurationValueRecordMapper; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.OrientationRecordDynamicSqlSupport; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.OrientationRecordMapper; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ConfigurationAttributeRecord; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationAttributeDAO; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.DAOLoggingSupport; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ResourceNotFoundException; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.TransactionHandler; + +@Lazy +@Component +@WebServiceProfile +public class ConfigurationAttributeDAOImpl implements ConfigurationAttributeDAO { + + private final ConfigurationAttributeRecordMapper configurationAttributeRecordMapper; + private final ConfigurationValueRecordMapper configurationValueRecordMapper; + private final OrientationRecordMapper orientationRecordMapper; + + protected ConfigurationAttributeDAOImpl( + final ConfigurationAttributeRecordMapper configurationAttributeRecordMapper, + final ConfigurationValueRecordMapper configurationValueRecordMapper, + final OrientationRecordMapper orientationRecordMapper) { + + this.configurationAttributeRecordMapper = configurationAttributeRecordMapper; + this.configurationValueRecordMapper = configurationValueRecordMapper; + this.orientationRecordMapper = orientationRecordMapper; + } + + @Override + public EntityType entityType() { + return EntityType.CONFIGURATION_ATTRIBUTE; + } + + @Override + @Transactional(readOnly = true) + public Result byPK(final Long id) { + return recordById(id) + .flatMap(ConfigurationAttributeDAOImpl::toDomainModel); + } + + @Override + @Transactional(readOnly = true) + public Result> allOf(final Set pks) { + return Result.tryCatch(() -> this.configurationAttributeRecordMapper.selectByExample() + .where(ConfigurationAttributeRecordDynamicSqlSupport.id, isIn(new ArrayList<>(pks))) + .build() + .execute() + .stream() + .map(ConfigurationAttributeDAOImpl::toDomainModel) + .flatMap(DAOLoggingSupport::logAndSkipOnError) + .collect(Collectors.toList())); + } + + @Override + @Transactional(readOnly = true) + public Result> allMatching( + final FilterMap filterMap, + final Predicate predicate) { + + return Result.tryCatch(() -> this.configurationAttributeRecordMapper + .selectByExample() + .where( + ConfigurationAttributeRecordDynamicSqlSupport.parentId, + SqlBuilder.isEqualToWhenPresent(filterMap.getConfigAttributeParentId())) + .and( + ConfigurationAttributeRecordDynamicSqlSupport.type, + SqlBuilder.isEqualToWhenPresent(filterMap.getConfigAttributeType())) + .build() + .execute() + .stream() + .map(ConfigurationAttributeDAOImpl::toDomainModel) + .flatMap(DAOLoggingSupport::logAndSkipOnError) + .filter(predicate) + .collect(Collectors.toList())); + } + + @Override + @Transactional(readOnly = true) + public Result> allChildAttributes(final Long parentId) { + return Result.tryCatch(() -> this.configurationAttributeRecordMapper + .selectByExample() + .where( + ConfigurationAttributeRecordDynamicSqlSupport.parentId, + SqlBuilder.isEqualTo(parentId)) + .build() + .execute() + .stream() + .map(ConfigurationAttributeDAOImpl::toDomainModel) + .flatMap(DAOLoggingSupport::logAndSkipOnError) + .collect(Collectors.toList())); + } + + @Override + @Transactional(readOnly = true) + public Result> getAllRootAttributes() { + return Result.tryCatch(() -> this.configurationAttributeRecordMapper + .selectByExample() + .where( + ConfigurationAttributeRecordDynamicSqlSupport.parentId, + SqlBuilder.isNull()) + .build() + .execute() + .stream() + .map(ConfigurationAttributeDAOImpl::toDomainModel) + .flatMap(DAOLoggingSupport::logAndSkipOnError) + .collect(Collectors.toList())); + } + + @Override + @Transactional + public Result createNew(final ConfigurationAttribute data) { + return Result.tryCatch(() -> { + final ConfigurationAttributeRecord newRecord = new ConfigurationAttributeRecord( + null, + data.name, + data.type.name(), + data.parentId, + data.resources, + data.validator, + data.dependencies, + data.defaultValue); + + this.configurationAttributeRecordMapper.insert(newRecord); + return newRecord; + }) + .flatMap(ConfigurationAttributeDAOImpl::toDomainModel) + .onError(TransactionHandler::rollback); + } + + @Override + @Transactional + public Result save(final ConfigurationAttribute data) { + return Result.tryCatch(() -> { + + final ConfigurationAttributeRecord newRecord = new ConfigurationAttributeRecord( + data.id, + data.name, + data.type.name(), + data.parentId, + data.resources, + data.validator, + data.dependencies, + data.defaultValue); + + this.configurationAttributeRecordMapper.updateByPrimaryKeySelective(newRecord); + return this.configurationAttributeRecordMapper.selectByPrimaryKey(data.id); + }) + .flatMap(ConfigurationAttributeDAOImpl::toDomainModel) + .onError(TransactionHandler::rollback); + } + + @Override + @Transactional + public Result> delete(final Set all) { + return Result.tryCatch(() -> { + + final List ids = extractListOfPKs(all); + final List result = new ArrayList<>(); + + // if this is a complex attribute that has children, delete the children first + final List children = + this.configurationAttributeRecordMapper.selectByExample() + .where(ConfigurationAttributeRecordDynamicSqlSupport.parentId, isIn(ids)) + .build() + .execute(); + + // recursive call for children and adding result to the overall result set + if (children != null && !children.isEmpty()) { + result.addAll(delete(children.stream() + .map(r -> new EntityKey(r.getId(), EntityType.CONFIGURATION_ATTRIBUTE)) + .collect(Collectors.toSet())) + .getOrThrow()); + } + + // delete all ConfigurationValue's that belongs to the ConfigurationAttributes to delete + this.configurationValueRecordMapper.deleteByExample() + .where( + ConfigurationValueRecordDynamicSqlSupport.configurationAttributeId, + SqlBuilder.isIn(ids)) + .build() + .execute(); + + // delete all Orientations that belongs to the ConfigurationAttributes to delete + this.orientationRecordMapper.deleteByExample() + .where( + OrientationRecordDynamicSqlSupport.configAttributeId, + SqlBuilder.isIn(ids)) + .build() + .execute(); + + // then delete all requested ConfigurationAttributes + this.configurationAttributeRecordMapper.deleteByExample() + .where(ConfigurationAttributeRecordDynamicSqlSupport.id, isIn(ids)) + .build() + .execute(); + + result.addAll(ids.stream() + .map(id -> new EntityKey(id, EntityType.CONFIGURATION_ATTRIBUTE)) + .collect(Collectors.toList())); + + return result; + }); + } + + Result recordById(final Long id) { + return Result.tryCatch(() -> { + final ConfigurationAttributeRecord record = this.configurationAttributeRecordMapper + .selectByPrimaryKey(id); + if (record == null) { + throw new ResourceNotFoundException( + EntityType.CONFIGURATION_ATTRIBUTE, + String.valueOf(id)); + } + return record; + }); + } + + static Result toDomainModel(final ConfigurationAttributeRecord record) { + return Result.tryCatch(() -> new ConfigurationAttribute( + record.getId(), + record.getParentId(), + record.getName(), + AttributeType.valueOf(record.getType()), + record.getResources(), + record.getValidator(), + record.getDependencies(), + record.getDefaultValue())); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ConfigurationDAOBatchService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ConfigurationDAOBatchService.java index 90d7a719..c24ee4bd 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ConfigurationDAOBatchService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ConfigurationDAOBatchService.java @@ -1,843 +1,837 @@ -/* - * 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.dao.impl; - -import static org.mybatis.dynamic.sql.SqlBuilder.isEqualTo; -import static org.mybatis.dynamic.sql.SqlBuilder.isNotEqualTo; - -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.function.Function; -import java.util.stream.Collectors; - -import org.apache.commons.lang3.BooleanUtils; -import org.apache.commons.lang3.StringUtils; -import org.joda.time.DateTime; -import org.joda.time.DateTimeZone; -import org.mybatis.dynamic.sql.SqlBuilder; -import org.mybatis.spring.SqlSessionTemplate; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.context.annotation.DependsOn; -import org.springframework.context.annotation.Lazy; -import org.springframework.stereotype.Component; - -import ch.ethz.seb.sebserver.gbl.api.APIMessage.FieldValidationException; -import ch.ethz.seb.sebserver.gbl.api.EntityType; -import ch.ethz.seb.sebserver.gbl.model.sebconfig.AttributeType; -import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigCreationInfo; -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.ConfigurationNode; -import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode.ConfigurationStatus; -import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationTableValues; -import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationTableValues.TableValue; -import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; -import ch.ethz.seb.sebserver.gbl.util.Result; -import ch.ethz.seb.sebserver.gbl.util.Utils; -import ch.ethz.seb.sebserver.webservice.datalayer.batis.BatisConfig; -import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ConfigurationAttributeRecordDynamicSqlSupport; -import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ConfigurationAttributeRecordMapper; -import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ConfigurationNodeRecordDynamicSqlSupport; -import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ConfigurationNodeRecordMapper; -import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ConfigurationRecordDynamicSqlSupport; -import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ConfigurationRecordMapper; -import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ConfigurationValueRecordDynamicSqlSupport; -import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ConfigurationValueRecordMapper; -import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ConfigurationAttributeRecord; -import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ConfigurationNodeRecord; -import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ConfigurationRecord; -import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ConfigurationValueRecord; -import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ResourceNotFoundException; -import ch.ethz.seb.sebserver.webservice.servicelayer.dao.TransactionHandler; -import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ExamConfigInitService; - -/** This service is internally used to implement MyBatis batch functionality for the most - * intensive write operation on Configuration domain. */ -@Lazy -@Component -@WebServiceProfile -@DependsOn("batisConfig") -class ConfigurationDAOBatchService { - - private static final Logger log = LoggerFactory.getLogger(ConfigurationDAOBatchService.class); - - public static final String INITIAL_VERSION_NAME = "v0"; - - private final ConfigurationNodeRecordMapper batchConfigurationNodeRecordMapper; - private final ConfigurationValueRecordMapper batchConfigurationValueRecordMapper; - private final ConfigurationAttributeRecordMapper batchConfigurationAttributeRecordMapper; - private final ConfigurationRecordMapper batchConfigurationRecordMapper; - private final ExamConfigInitService examConfigInitService; - - private final SqlSessionTemplate batchSqlSessionTemplate; - - protected ConfigurationDAOBatchService( - @Qualifier(BatisConfig.SQL_BATCH_SESSION_TEMPLATE) final SqlSessionTemplate batchSqlSessionTemplate, - final ExamConfigInitService examConfigInitService) { - - final org.apache.ibatis.session.Configuration batisConfig = - batchSqlSessionTemplate.getConfiguration(); - this.examConfigInitService = examConfigInitService; - - log.info("Registered MyBatis Mappers: {}", batisConfig.getMapperRegistry().getMappers()); - - // NOTE: sometimes this mapper was not registered on startup. No reason why. Force loading if absent. - if (!batisConfig.hasMapper(ConfigurationNodeRecordMapper.class)) { - batisConfig.addMapper(ConfigurationNodeRecordMapper.class); - } - - if (!batisConfig.hasMapper(ConfigurationValueRecordMapper.class)) { - batisConfig.addMapper(ConfigurationValueRecordMapper.class); - } - - if (!batisConfig.hasMapper(ConfigurationAttributeRecordMapper.class)) { - batisConfig.addMapper(ConfigurationAttributeRecordMapper.class); - } - - if (!batisConfig.hasMapper(ConfigurationRecordMapper.class)) { - batisConfig.addMapper(ConfigurationRecordMapper.class); - } - - this.batchConfigurationNodeRecordMapper = - batchSqlSessionTemplate.getMapper(ConfigurationNodeRecordMapper.class); - this.batchConfigurationValueRecordMapper = - batchSqlSessionTemplate.getMapper(ConfigurationValueRecordMapper.class); - this.batchConfigurationAttributeRecordMapper = - batchSqlSessionTemplate.getMapper(ConfigurationAttributeRecordMapper.class); - this.batchConfigurationRecordMapper = - batchSqlSessionTemplate.getMapper(ConfigurationRecordMapper.class); - this.batchSqlSessionTemplate = batchSqlSessionTemplate; - - } - - Result createNewConfiguration(final ConfigurationNode data) { - return Result.tryCatch(() -> { - - final Long count = this.batchConfigurationNodeRecordMapper.countByExample() - .where( - ConfigurationNodeRecordDynamicSqlSupport.name, - isEqualTo(data.name)) - .and( - ConfigurationNodeRecordDynamicSqlSupport.institutionId, - SqlBuilder.isEqualTo(data.institutionId)) - .build() - .execute(); - - if (count != null && count.longValue() > 0) { - throw new FieldValidationException("name", "configurationNode:name:exists"); - } - - final ConfigurationNodeRecord newRecord = new ConfigurationNodeRecord( - null, - data.institutionId, - data.templateId, - data.owner, - data.name, - data.description, - data.type.name(), - (data.status != null) ? data.status.name() : ConfigurationStatus.CONSTRUCTION.name()); - - this.batchConfigurationNodeRecordMapper.insert(newRecord); - this.batchSqlSessionTemplate.flushStatements(); - return newRecord; - }) - .flatMap(ConfigurationNodeDAOImpl::toDomainModel) - .flatMap(this::createInitialConfiguration); - } - - Result saveNewTableValues(final ConfigurationTableValues value) { - return checkInstitutionalIntegrity(value) - .map(this::checkFollowUpIntegrity) - .map(val -> { - final ConfigurationAttributeRecord record = this.batchConfigurationAttributeRecordMapper - .selectByPrimaryKey(val.attributeId); - if (record == null) { - throw new ResourceNotFoundException( - EntityType.CONFIGURATION_ATTRIBUTE, - String.valueOf(val.attributeId)); - } - return record; - }) - .map(attributeRecord -> { - - final String type = attributeRecord.getType(); - if (AttributeType.TABLE.name().equals(type)) { - saveAsTable(value, attributeRecord); - } else { - saveAsComposite(value); - } - - return value; - }); - } - - Result saveToHistory(final Long configurationNodeId) { - return Result.tryCatch(() -> { - - // get follow-up configuration... - final ConfigurationRecord followupConfig = getFollowupConfigurationRecord(configurationNodeId); - - // with actual attribute values - final List allValues = this.batchConfigurationValueRecordMapper - .selectByExample() - .where( - ConfigurationValueRecordDynamicSqlSupport.configurationId, - isEqualTo(followupConfig.getId())) - .build() - .execute(); - - // get current versions count - - // close follow-up configuration to save in history - final ConfigurationRecord configUpdate = new ConfigurationRecord( - followupConfig.getId(), - null, - null, - generateVersionName(configurationNodeId), - DateTime.now(DateTimeZone.UTC), - BooleanUtils.toInteger(false)); - this.batchConfigurationRecordMapper.updateByPrimaryKeySelective(configUpdate); - - this.batchSqlSessionTemplate.flushStatements(); - - // and create a new follow-up... - final ConfigurationRecord newFollowup = new ConfigurationRecord( - null, - followupConfig.getInstitutionId(), - followupConfig.getConfigurationNodeId(), - null, - null, - BooleanUtils.toInteger(true)); - this.batchConfigurationRecordMapper.insert(newFollowup); - - this.batchSqlSessionTemplate.flushStatements(); - - // with the current attribute values - allValues.stream() - .map(oldValRec -> new ConfigurationValueRecord( - null, - oldValRec.getInstitutionId(), - newFollowup.getId(), - oldValRec.getConfigurationAttributeId(), - oldValRec.getListIndex(), - oldValRec.getValue())) - .forEach(newValRec -> this.batchConfigurationValueRecordMapper.insert(newValRec)); - - return this.batchConfigurationRecordMapper - .selectByPrimaryKey(newFollowup.getId()); - - }) - .flatMap(ConfigurationDAOImpl::toDomainModel); - } - - private String generateVersionName(final Long configurationNodeId) { - final Long versions = this.batchConfigurationRecordMapper - .countByExample() - .where( - ConfigurationRecordDynamicSqlSupport.configurationNodeId, - isEqualTo(configurationNodeId)) - .and( - ConfigurationRecordDynamicSqlSupport.followup, - isNotEqualTo(BooleanUtils.toInteger(true))) - .build() - .execute(); - return "v" + versions; - } - - Result undo(final Long configurationNodeId) { - return Result.tryCatch(() -> { - // get all configurations of the node - final List configs = this.batchConfigurationRecordMapper - .selectByExample() - .where( - ConfigurationRecordDynamicSqlSupport.configurationNodeId, - isEqualTo(configurationNodeId)) - .orderBy(ConfigurationRecordDynamicSqlSupport.versionDate) - .build() - .execute(); - - return configs.get(configs.size() - 1); - }) - .flatMap(rec -> restoreToVersion(configurationNodeId, rec.getId())); - } - - Result restoreToDefaultValues(final Long configurationNodeId) { - return Result.tryCatch(() -> { - // get initial version that contains the default values either from base or from template - return this.batchConfigurationRecordMapper.selectIdsByExample() - .where( - ConfigurationRecordDynamicSqlSupport.configurationNodeId, - isEqualTo(configurationNodeId)) - .and( - ConfigurationRecordDynamicSqlSupport.version, - isEqualTo(INITIAL_VERSION_NAME)) - .build() - .execute() - .stream() - .collect(Utils.toSingleton()); - - }) - .flatMap(configId -> restoreToVersion(configurationNodeId, configId)); - } - - Result restoreToVersion(final Long configurationNodeId, final Long configId) { - return Result.tryCatch(() -> { - - // get requested configuration in history... - final ConfigurationRecord config = this.batchConfigurationRecordMapper - .selectByExample() - .where( - ConfigurationRecordDynamicSqlSupport.configurationNodeId, - isEqualTo(configurationNodeId)) - .and( - ConfigurationRecordDynamicSqlSupport.id, - isEqualTo(configId)) - .build() - .execute() - .stream() - .collect(Utils.toSingleton()); - - // with historic attribute values - final List historicValues = this.batchConfigurationValueRecordMapper - .selectByExample() - .where( - ConfigurationValueRecordDynamicSqlSupport.configurationId, - isEqualTo(config.getId())) - .build() - .execute(); - - // get follow-up configuration id - final ConfigurationRecord followup = getFollowupConfigurationRecord(configurationNodeId); - - // delete all values of the follow-up - this.batchConfigurationValueRecordMapper - .deleteByExample() - .where( - ConfigurationValueRecordDynamicSqlSupport.configurationId, - isEqualTo(followup.getId())) - .build() - .execute(); - - // restore all current values of the follow-up with historic values - historicValues.stream() - .map(historicValRec -> new ConfigurationValueRecord( - null, - followup.getInstitutionId(), - followup.getId(), - historicValRec.getConfigurationAttributeId(), - historicValRec.getListIndex(), - historicValRec.getValue())) - .forEach(newValRec -> this.batchConfigurationValueRecordMapper.insert(newValRec)); - - return followup; - }) - .flatMap(ConfigurationDAOImpl::toDomainModel); - } - - Result createCopy( - final Long institutionId, - final String newOwner, - final ConfigCreationInfo copyInfo) { - - return Result.tryCatch(() -> { - - final Long count = this.batchConfigurationNodeRecordMapper.countByExample() - .where( - ConfigurationNodeRecordDynamicSqlSupport.name, - isEqualTo(copyInfo.name)) - .and( - ConfigurationNodeRecordDynamicSqlSupport.institutionId, - isEqualTo(institutionId)) - .build() - .execute(); - - if (count != null && count.longValue() > 0) { - throw new FieldValidationException("name", "configurationNode:name:exists"); - } - - final ConfigurationNodeRecord sourceNode = this.batchConfigurationNodeRecordMapper - .selectByPrimaryKey(copyInfo.configurationNodeId); - - if (!sourceNode.getInstitutionId().equals(institutionId)) { - throw new IllegalArgumentException("Institution integrity violation"); - } - - return this.copyNodeRecord(sourceNode, newOwner, copyInfo); - }) - .flatMap(ConfigurationNodeDAOImpl::toDomainModel) - .onError(TransactionHandler::rollback); - } - - private ConfigurationNodeRecord copyNodeRecord( - final ConfigurationNodeRecord nodeRec, - final String newOwner, - final ConfigCreationInfo copyInfo) { - - final ConfigurationNodeRecord newNodeRec = new ConfigurationNodeRecord( - null, - nodeRec.getInstitutionId(), - nodeRec.getTemplateId(), - StringUtils.isNotBlank(newOwner) ? newOwner : nodeRec.getOwner(), - copyInfo.getName(), - copyInfo.getDescription(), - copyInfo.configurationType.name(), - ConfigurationStatus.CONSTRUCTION.name()); - - this.batchConfigurationNodeRecordMapper.insert(newNodeRec); - this.batchSqlSessionTemplate.flushStatements(); - - final List configs = this.batchConfigurationRecordMapper - .selectByExample() - .where( - ConfigurationRecordDynamicSqlSupport.configurationNodeId, - isEqualTo(nodeRec.getId())) - .build() - .execute(); - - if (BooleanUtils.toBoolean(copyInfo.withHistory)) { - configs - .stream() - .forEach(configRec -> this.copyConfiguration( - configRec.getInstitutionId(), - configRec.getId(), - newNodeRec.getId())); - } else { - configs - .stream() - .filter(configRec -> configRec.getVersionDate() == null) - .findFirst() - .ifPresent(configRec -> { - // No history means to create a first version and a follow-up with the copied values - final ConfigurationRecord newFirstVersion = new ConfigurationRecord( - null, - configRec.getInstitutionId(), - newNodeRec.getId(), - ConfigurationDAOBatchService.INITIAL_VERSION_NAME, - DateTime.now(DateTimeZone.UTC), - BooleanUtils.toInteger(false)); - this.batchConfigurationRecordMapper.insert(newFirstVersion); - this.batchSqlSessionTemplate.flushStatements(); - this.copyValues( - configRec.getInstitutionId(), - configRec.getId(), - newFirstVersion.getId()); - // and copy the follow-up - final ConfigurationRecord followup = new ConfigurationRecord( - null, - configRec.getInstitutionId(), - newNodeRec.getId(), - null, - null, - BooleanUtils.toInteger(true)); - this.batchConfigurationRecordMapper.insert(followup); - this.batchSqlSessionTemplate.flushStatements(); - this.copyValues( - configRec.getInstitutionId(), - configRec.getId(), - followup.getId()); - }); - } - - this.batchSqlSessionTemplate.flushStatements(); - return newNodeRec; - } - - private Result copyConfiguration( - final Long institutionId, - final Long fromConfigurationId, - final Long toConfigurationNodeId) { - - return Result.tryCatch(() -> { - final ConfigurationRecord fromRecord = this.batchConfigurationRecordMapper - .selectByPrimaryKey(fromConfigurationId); - - if (!fromRecord.getInstitutionId().equals(institutionId)) { - throw new IllegalArgumentException("Institution integrity violation"); - } - - final ConfigurationRecord configurationRecord = new ConfigurationRecord( - null, - fromRecord.getInstitutionId(), - toConfigurationNodeId, - fromRecord.getVersion(), - fromRecord.getVersionDate(), - fromRecord.getFollowup()); - this.batchConfigurationRecordMapper.insert(configurationRecord); - this.batchSqlSessionTemplate.flushStatements(); - return configurationRecord; - }) - .flatMap(ConfigurationDAOImpl::toDomainModel) - .map(newConfig -> { - this.copyValues( - institutionId, - fromConfigurationId, - newConfig.getId()); - return newConfig; - }); - } - - private void copyValues( - final Long institutionId, - final Long fromConfigId, - final Long toConfigId) { - - this.batchConfigurationValueRecordMapper - .selectByExample() - .where( - ConfigurationValueRecordDynamicSqlSupport.institutionId, - isEqualTo(institutionId)) - .and( - ConfigurationValueRecordDynamicSqlSupport.configurationId, - isEqualTo(fromConfigId)) - .build() - .execute() - .stream() - .map(fromRec -> new ConfigurationValueRecord( - null, - fromRec.getInstitutionId(), - toConfigId, - fromRec.getConfigurationAttributeId(), - fromRec.getListIndex(), - fromRec.getValue())) - .forEach(this.batchConfigurationValueRecordMapper::insert); - } - - private ConfigurationRecord getFollowupConfigurationRecord(final Long configurationNodeId) { - return this.batchConfigurationRecordMapper - .selectByExample() - .where( - ConfigurationRecordDynamicSqlSupport.configurationNodeId, - isEqualTo(configurationNodeId)) - .and( - ConfigurationRecordDynamicSqlSupport.followup, - isEqualTo(BooleanUtils.toInteger(true))) - .build() - .execute() - .stream() - .collect(Utils.toSingleton()); - } - - private void saveAsTable( - final ConfigurationTableValues value, - final ConfigurationAttributeRecord attributeRecord) { - - final Map attributeMap = - this.batchConfigurationAttributeRecordMapper - .selectByExample() - .where( - ConfigurationAttributeRecordDynamicSqlSupport.parentId, - isEqualTo(attributeRecord.getId())) - .build() - .execute() - .stream() - .collect(Collectors.toMap(rec -> rec.getId(), Function.identity())); - - final List columnAttributeIds = attributeMap.values() - .stream() - .map(a -> a.getId()) - .collect(Collectors.toList()); - - // first delete all old values of this table - this.batchConfigurationValueRecordMapper.deleteByExample() - .where( - ConfigurationValueRecordDynamicSqlSupport.configurationId, - isEqualTo(value.configurationId)) - .and( - ConfigurationValueRecordDynamicSqlSupport.configurationAttributeId, - SqlBuilder.isIn(columnAttributeIds)) - .build() - .execute(); - - // then add the new values - for (final TableValue tableValue : value.values) { - final ConfigurationAttributeRecord columnAttr = attributeMap.get(tableValue.attributeId); - final ConfigurationValueRecord valueRecord = new ConfigurationValueRecord( - null, - value.institutionId, - value.configurationId, - columnAttr.getId(), - tableValue.listIndex, - tableValue.value); - - this.batchConfigurationValueRecordMapper.insertSelective(valueRecord); - } - } - - private void saveAsComposite(final ConfigurationTableValues value) { - for (final TableValue tableValue : value.values) { - - final List valuePK = this.batchConfigurationValueRecordMapper.selectIdsByExample() - .where( - ConfigurationValueRecordDynamicSqlSupport.configurationId, - isEqualTo(value.configurationId)) - .and( - ConfigurationValueRecordDynamicSqlSupport.configurationAttributeId, - isEqualTo(tableValue.attributeId)) - .and( - ConfigurationValueRecordDynamicSqlSupport.listIndex, - isEqualTo(tableValue.listIndex)) - .build() - .execute(); - - if (valuePK != null && valuePK.size() > 1) { - throw new IllegalStateException("Expected no more then one element"); - } - - if (valuePK == null || valuePK.isEmpty()) { - // insert - this.batchConfigurationValueRecordMapper.insert( - new ConfigurationValueRecord( - null, - value.institutionId, - value.configurationId, - tableValue.attributeId, - tableValue.listIndex, - tableValue.value)); - } else { - // update - this.batchConfigurationValueRecordMapper.updateByPrimaryKey( - new ConfigurationValueRecord( - valuePK.iterator().next(), - value.institutionId, - value.configurationId, - tableValue.attributeId, - tableValue.listIndex, - tableValue.value)); - } - } - } - - private Result checkInstitutionalIntegrity(final ConfigurationTableValues data) { - return Result.tryCatch(() -> { - final ConfigurationRecord r = this.batchConfigurationRecordMapper.selectByPrimaryKey(data.configurationId); - if (r.getInstitutionId().longValue() != data.institutionId.longValue()) { - throw new IllegalArgumentException("Institutional integrity constraint violation"); - } - return data; - }); - } - - private ConfigurationTableValues checkFollowUpIntegrity(final ConfigurationTableValues data) { - checkFollowUp(data.configurationId); - return data; - } - - private void checkFollowUp(final Long configurationId) { - final ConfigurationRecord config = this.batchConfigurationRecordMapper - .selectByPrimaryKey(configurationId); - - if (!BooleanUtils.toBoolean(config.getFollowup())) { - throw new IllegalArgumentException( - "Forbidden to modify an configuration value of a none follow-up configuration"); - } - } - - /* - * Creates the first Configuration and a follow-up within a newly created ConfigurationNode. - * This creates a first new Configuration and all attribute values for that either - * from the default values or from template values if defined. - * Then a follow-up Configuration is created with the same values to follow-up user input - */ - private Result createInitialConfiguration(final ConfigurationNode config) { - return Result.tryCatch(() -> { - - final ConfigurationRecord initConfig = new ConfigurationRecord( - null, - config.institutionId, - config.id, - INITIAL_VERSION_NAME, - DateTime.now(DateTimeZone.UTC), - BooleanUtils.toInteger(false)); - - this.batchConfigurationRecordMapper.insert(initConfig); - this.batchSqlSessionTemplate.flushStatements(); - - createAttributeValues(config, initConfig) - .getOrThrow(); - - final ConfigurationRecord followup = new ConfigurationRecord( - null, - config.institutionId, - config.id, - null, - null, - BooleanUtils.toInteger(true)); - - this.batchConfigurationRecordMapper.insert(followup); - this.batchSqlSessionTemplate.flushStatements(); - - this.copyValues( - config.institutionId, - initConfig.getId(), - followup.getId()); - - return config; - }); - } - - /* - * Creates all attribute values for a given ConfigurationNode with its newly created first ConfigurationRecord - * If the ConfigurationNode has a templateId this will gather all attributes values from the latest - * configuration of this ConfigurationNode template to override the default values. - * Otherwise creates all attribute values from the default values. - */ - private Result createAttributeValues( - final ConfigurationNode configNode, - final ConfigurationRecord config) { - - return Result.tryCatch(() -> { - - // go through all configuration attributes and create and store the default value - this.batchConfigurationAttributeRecordMapper - .selectByExample() - .build() - .execute() - .stream() - // filter child attributes of tables. No default value for tables. Use templates for that - .filter(ConfigurationDAOBatchService::filterChildAttribute) - .forEach(attrRec -> { - this.batchConfigurationValueRecordMapper.insert(new ConfigurationValueRecord( - null, - configNode.institutionId, - config.getId(), - attrRec.getId(), - 0, - attrRec.getDefaultValue())); - }); - - // override with template values if available - if (configNode.templateId == null || configNode.templateId.equals(ConfigurationNode.DEFAULT_TEMPLATE_ID)) { - initAdditionalDefaultValues(configNode, config); - } else { - writeTemplateValues(configNode, config); - } - - return configNode; - }); - } - - private void initAdditionalDefaultValues( - final ConfigurationNode configNode, - final ConfigurationRecord config) { - - // get all attributes and map the names to id's - final Map attributeMap = this.batchConfigurationAttributeRecordMapper - .selectByExample() - .build() - .execute() - .stream() - .map(ConfigurationAttributeDAOImpl::toDomainModel) - .map(Result::get) - .filter(Objects::nonNull) - .collect(Collectors.toMap( - attr -> attr.name, - Function.identity())); - - this.examConfigInitService.getAdditionalDefaultValues( - configNode.institutionId, - config.getId(), - attributeMap::get) - .stream() - .forEach(value -> { - final ConfigurationValueRecord valueRec = new ConfigurationValueRecord( - null, - value.institutionId, - value.configurationId, - value.attributeId, - value.listIndex, - value.value); - - this.batchConfigurationValueRecordMapper.insert(valueRec); - }); - - this.batchSqlSessionTemplate.flushStatements(); - } - - private void writeTemplateValues( - final ConfigurationNode configNode, - final ConfigurationRecord config) { - - final List templateValues = getTemplateValues(configNode); - templateValues.stream() - .forEach(templateValue -> { - final Long existingId = this.batchConfigurationValueRecordMapper - .selectIdsByExample() - .where( - ConfigurationValueRecordDynamicSqlSupport.configurationId, - isEqualTo(config.getId())) - .and( - ConfigurationValueRecordDynamicSqlSupport.configurationAttributeId, - isEqualTo(templateValue.getConfigurationAttributeId())) - .and( - ConfigurationValueRecordDynamicSqlSupport.listIndex, - isEqualTo(templateValue.getListIndex())) - .build() - .execute() - .stream() - .findFirst() - .orElse(null); - - final ConfigurationValueRecord valueRec = new ConfigurationValueRecord( - existingId, - configNode.institutionId, - config.getId(), - templateValue.getConfigurationAttributeId(), - templateValue.getListIndex(), - templateValue.getValue()); - - if (existingId != null) { - this.batchConfigurationValueRecordMapper.updateByPrimaryKey(valueRec); - } else { - this.batchConfigurationValueRecordMapper.insert(valueRec); - } - }); - - this.batchSqlSessionTemplate.flushStatements(); - } - - private static boolean filterChildAttribute(final ConfigurationAttributeRecord rec) { - - if (rec.getParentId() == null) { - return true; - } - - return BooleanUtils.toBoolean(ConfigurationAttribute.getDependencyValue( - ConfigurationAttribute.DEPENDENCY_CREATE_DEFAULT_VALUE, - rec.getDependencies())); - } - - /* - * Get values from template with configuration attribute id mapped to the value - * returns empty list if no template available - */ - private List getTemplateValues(final ConfigurationNode configNode) { - if (configNode.templateId == null || configNode.templateId.equals(ConfigurationNode.DEFAULT_TEMPLATE_ID)) { - return Collections.emptyList(); - } - - final Long configurationId = this.batchConfigurationRecordMapper.selectByExample() - .where(ConfigurationRecordDynamicSqlSupport.configurationNodeId, isEqualTo(configNode.templateId)) - .and(ConfigurationRecordDynamicSqlSupport.followup, isEqualTo(BooleanUtils.toIntegerObject(true))) - .build() - .execute() - .stream() - .collect(Utils.toSingleton()) - .getId(); - - return this.batchConfigurationValueRecordMapper.selectByExample() - .where(ConfigurationValueRecordDynamicSqlSupport.configurationId, isEqualTo(configurationId)) - .build() - .execute(); - } - -} +/* + * 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.dao.impl; + +import static org.mybatis.dynamic.sql.SqlBuilder.isEqualTo; +import static org.mybatis.dynamic.sql.SqlBuilder.isNotEqualTo; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.StringUtils; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.mybatis.dynamic.sql.SqlBuilder; +import org.mybatis.spring.SqlSessionTemplate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.DependsOn; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +import ch.ethz.seb.sebserver.gbl.api.APIMessage.FieldValidationException; +import ch.ethz.seb.sebserver.gbl.api.EntityType; +import ch.ethz.seb.sebserver.gbl.model.sebconfig.AttributeType; +import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigCreationInfo; +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.ConfigurationNode; +import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode.ConfigurationStatus; +import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationTableValues; +import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationTableValues.TableValue; +import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; +import ch.ethz.seb.sebserver.gbl.util.Result; +import ch.ethz.seb.sebserver.gbl.util.Utils; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.BatisConfig; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ConfigurationAttributeRecordDynamicSqlSupport; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ConfigurationAttributeRecordMapper; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ConfigurationNodeRecordDynamicSqlSupport; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ConfigurationNodeRecordMapper; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ConfigurationRecordDynamicSqlSupport; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ConfigurationRecordMapper; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ConfigurationValueRecordDynamicSqlSupport; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ConfigurationValueRecordMapper; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ConfigurationAttributeRecord; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ConfigurationNodeRecord; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ConfigurationRecord; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ConfigurationValueRecord; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ResourceNotFoundException; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.TransactionHandler; +import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ExamConfigInitService; + +/** This service is internally used to implement MyBatis batch functionality for the most + * intensive write operation on Configuration domain. */ +@Lazy +@Component +@WebServiceProfile +@DependsOn("batisConfig") +class ConfigurationDAOBatchService { + + private static final Logger log = LoggerFactory.getLogger(ConfigurationDAOBatchService.class); + + public static final String INITIAL_VERSION_NAME = "v0"; + + private final ConfigurationNodeRecordMapper batchConfigurationNodeRecordMapper; + private final ConfigurationValueRecordMapper batchConfigurationValueRecordMapper; + private final ConfigurationAttributeRecordMapper batchConfigurationAttributeRecordMapper; + private final ConfigurationRecordMapper batchConfigurationRecordMapper; + private final ExamConfigInitService examConfigInitService; + + private final SqlSessionTemplate batchSqlSessionTemplate; + + protected ConfigurationDAOBatchService( + @Qualifier(BatisConfig.SQL_BATCH_SESSION_TEMPLATE) final SqlSessionTemplate batchSqlSessionTemplate, + final ExamConfigInitService examConfigInitService) { + + final org.apache.ibatis.session.Configuration batisConfig = + batchSqlSessionTemplate.getConfiguration(); + this.examConfigInitService = examConfigInitService; + + log.info("Registered MyBatis Mappers: {}", batisConfig.getMapperRegistry().getMappers()); + + // NOTE: sometimes this mapper was not registered on startup. No reason why. Force loading if absent. + if (!batisConfig.hasMapper(ConfigurationNodeRecordMapper.class)) { + batisConfig.addMapper(ConfigurationNodeRecordMapper.class); + } + + if (!batisConfig.hasMapper(ConfigurationValueRecordMapper.class)) { + batisConfig.addMapper(ConfigurationValueRecordMapper.class); + } + + if (!batisConfig.hasMapper(ConfigurationAttributeRecordMapper.class)) { + batisConfig.addMapper(ConfigurationAttributeRecordMapper.class); + } + + if (!batisConfig.hasMapper(ConfigurationRecordMapper.class)) { + batisConfig.addMapper(ConfigurationRecordMapper.class); + } + + this.batchConfigurationNodeRecordMapper = + batchSqlSessionTemplate.getMapper(ConfigurationNodeRecordMapper.class); + this.batchConfigurationValueRecordMapper = + batchSqlSessionTemplate.getMapper(ConfigurationValueRecordMapper.class); + this.batchConfigurationAttributeRecordMapper = + batchSqlSessionTemplate.getMapper(ConfigurationAttributeRecordMapper.class); + this.batchConfigurationRecordMapper = + batchSqlSessionTemplate.getMapper(ConfigurationRecordMapper.class); + this.batchSqlSessionTemplate = batchSqlSessionTemplate; + + } + + Result createNewConfiguration(final ConfigurationNode data) { + return Result.tryCatch(() -> { + + final Long count = this.batchConfigurationNodeRecordMapper.countByExample() + .where( + ConfigurationNodeRecordDynamicSqlSupport.name, + isEqualTo(data.name)) + .and( + ConfigurationNodeRecordDynamicSqlSupport.institutionId, + SqlBuilder.isEqualTo(data.institutionId)) + .build() + .execute(); + + if (count != null && count > 0) { + throw new FieldValidationException("name", "configurationNode:name:exists"); + } + + final ConfigurationNodeRecord newRecord = new ConfigurationNodeRecord( + null, + data.institutionId, + data.templateId, + data.owner, + data.name, + data.description, + data.type.name(), + (data.status != null) ? data.status.name() : ConfigurationStatus.CONSTRUCTION.name()); + + this.batchConfigurationNodeRecordMapper.insert(newRecord); + this.batchSqlSessionTemplate.flushStatements(); + return newRecord; + }) + .flatMap(ConfigurationNodeDAOImpl::toDomainModel) + .flatMap(this::createInitialConfiguration); + } + + Result saveNewTableValues(final ConfigurationTableValues value) { + return checkInstitutionalIntegrity(value) + .map(this::checkFollowUpIntegrity) + .map(val -> { + final ConfigurationAttributeRecord record = this.batchConfigurationAttributeRecordMapper + .selectByPrimaryKey(val.attributeId); + if (record == null) { + throw new ResourceNotFoundException( + EntityType.CONFIGURATION_ATTRIBUTE, + String.valueOf(val.attributeId)); + } + return record; + }) + .map(attributeRecord -> { + + final String type = attributeRecord.getType(); + if (AttributeType.TABLE.name().equals(type)) { + saveAsTable(value, attributeRecord); + } else { + saveAsComposite(value); + } + + return value; + }); + } + + Result saveToHistory(final Long configurationNodeId) { + return Result.tryCatch(() -> { + + // get follow-up configuration... + final ConfigurationRecord followupConfig = getFollowupConfigurationRecord(configurationNodeId); + + // with actual attribute values + final List allValues = this.batchConfigurationValueRecordMapper + .selectByExample() + .where( + ConfigurationValueRecordDynamicSqlSupport.configurationId, + isEqualTo(followupConfig.getId())) + .build() + .execute(); + + // get current versions count + + // close follow-up configuration to save in history + final ConfigurationRecord configUpdate = new ConfigurationRecord( + followupConfig.getId(), + null, + null, + generateVersionName(configurationNodeId), + DateTime.now(DateTimeZone.UTC), + BooleanUtils.toInteger(false)); + this.batchConfigurationRecordMapper.updateByPrimaryKeySelective(configUpdate); + + this.batchSqlSessionTemplate.flushStatements(); + + // and create a new follow-up... + final ConfigurationRecord newFollowup = new ConfigurationRecord( + null, + followupConfig.getInstitutionId(), + followupConfig.getConfigurationNodeId(), + null, + null, + BooleanUtils.toInteger(true)); + this.batchConfigurationRecordMapper.insert(newFollowup); + + this.batchSqlSessionTemplate.flushStatements(); + + // with the current attribute values + allValues.stream() + .map(oldValRec -> new ConfigurationValueRecord( + null, + oldValRec.getInstitutionId(), + newFollowup.getId(), + oldValRec.getConfigurationAttributeId(), + oldValRec.getListIndex(), + oldValRec.getValue())) + .forEach(this.batchConfigurationValueRecordMapper::insert); + + return this.batchConfigurationRecordMapper + .selectByPrimaryKey(newFollowup.getId()); + + }) + .flatMap(ConfigurationDAOImpl::toDomainModel); + } + + private String generateVersionName(final Long configurationNodeId) { + final Long versions = this.batchConfigurationRecordMapper + .countByExample() + .where( + ConfigurationRecordDynamicSqlSupport.configurationNodeId, + isEqualTo(configurationNodeId)) + .and( + ConfigurationRecordDynamicSqlSupport.followup, + isNotEqualTo(BooleanUtils.toInteger(true))) + .build() + .execute(); + return "v" + versions; + } + + Result undo(final Long configurationNodeId) { + return Result.tryCatch(() -> { + // get all configurations of the node + final List configs = this.batchConfigurationRecordMapper + .selectByExample() + .where( + ConfigurationRecordDynamicSqlSupport.configurationNodeId, + isEqualTo(configurationNodeId)) + .orderBy(ConfigurationRecordDynamicSqlSupport.versionDate) + .build() + .execute(); + + return configs.get(configs.size() - 1); + }) + .flatMap(rec -> restoreToVersion(configurationNodeId, rec.getId())); + } + + Result restoreToDefaultValues(final Long configurationNodeId) { + return Result.tryCatch(() -> { + // get initial version that contains the default values either from base or from template + return this.batchConfigurationRecordMapper.selectIdsByExample() + .where( + ConfigurationRecordDynamicSqlSupport.configurationNodeId, + isEqualTo(configurationNodeId)) + .and( + ConfigurationRecordDynamicSqlSupport.version, + isEqualTo(INITIAL_VERSION_NAME)) + .build() + .execute() + .stream() + .collect(Utils.toSingleton()); + + }) + .flatMap(configId -> restoreToVersion(configurationNodeId, configId)); + } + + Result restoreToVersion(final Long configurationNodeId, final Long configId) { + return Result.tryCatch(() -> { + + // get requested configuration in history... + final ConfigurationRecord config = this.batchConfigurationRecordMapper + .selectByExample() + .where( + ConfigurationRecordDynamicSqlSupport.configurationNodeId, + isEqualTo(configurationNodeId)) + .and( + ConfigurationRecordDynamicSqlSupport.id, + isEqualTo(configId)) + .build() + .execute() + .stream() + .collect(Utils.toSingleton()); + + // with historic attribute values + final List historicValues = this.batchConfigurationValueRecordMapper + .selectByExample() + .where( + ConfigurationValueRecordDynamicSqlSupport.configurationId, + isEqualTo(config.getId())) + .build() + .execute(); + + // get follow-up configuration id + final ConfigurationRecord followup = getFollowupConfigurationRecord(configurationNodeId); + + // delete all values of the follow-up + this.batchConfigurationValueRecordMapper + .deleteByExample() + .where( + ConfigurationValueRecordDynamicSqlSupport.configurationId, + isEqualTo(followup.getId())) + .build() + .execute(); + + // restore all current values of the follow-up with historic values + historicValues.stream() + .map(historicValRec -> new ConfigurationValueRecord( + null, + followup.getInstitutionId(), + followup.getId(), + historicValRec.getConfigurationAttributeId(), + historicValRec.getListIndex(), + historicValRec.getValue())) + .forEach(this.batchConfigurationValueRecordMapper::insert); + + return followup; + }) + .flatMap(ConfigurationDAOImpl::toDomainModel); + } + + Result createCopy( + final Long institutionId, + final String newOwner, + final ConfigCreationInfo copyInfo) { + + return Result.tryCatch(() -> { + + final Long count = this.batchConfigurationNodeRecordMapper.countByExample() + .where( + ConfigurationNodeRecordDynamicSqlSupport.name, + isEqualTo(copyInfo.name)) + .and( + ConfigurationNodeRecordDynamicSqlSupport.institutionId, + isEqualTo(institutionId)) + .build() + .execute(); + + if (count != null && count > 0) { + throw new FieldValidationException("name", "configurationNode:name:exists"); + } + + final ConfigurationNodeRecord sourceNode = this.batchConfigurationNodeRecordMapper + .selectByPrimaryKey(copyInfo.configurationNodeId); + + if (!sourceNode.getInstitutionId().equals(institutionId)) { + throw new IllegalArgumentException("Institution integrity violation"); + } + + return this.copyNodeRecord(sourceNode, newOwner, copyInfo); + }) + .flatMap(ConfigurationNodeDAOImpl::toDomainModel) + .onError(TransactionHandler::rollback); + } + + private ConfigurationNodeRecord copyNodeRecord( + final ConfigurationNodeRecord nodeRec, + final String newOwner, + final ConfigCreationInfo copyInfo) { + + final ConfigurationNodeRecord newNodeRec = new ConfigurationNodeRecord( + null, + nodeRec.getInstitutionId(), + nodeRec.getTemplateId(), + StringUtils.isNotBlank(newOwner) ? newOwner : nodeRec.getOwner(), + copyInfo.getName(), + copyInfo.getDescription(), + copyInfo.configurationType.name(), + ConfigurationStatus.CONSTRUCTION.name()); + + this.batchConfigurationNodeRecordMapper.insert(newNodeRec); + this.batchSqlSessionTemplate.flushStatements(); + + final List configs = this.batchConfigurationRecordMapper + .selectByExample() + .where( + ConfigurationRecordDynamicSqlSupport.configurationNodeId, + isEqualTo(nodeRec.getId())) + .build() + .execute(); + + if (BooleanUtils.toBoolean(copyInfo.withHistory)) { + configs.forEach(configRec -> this.copyConfiguration( + configRec.getInstitutionId(), + configRec.getId(), + newNodeRec.getId())); + } else { + configs + .stream() + .filter(configRec -> configRec.getVersionDate() == null) + .findFirst() + .ifPresent(configRec -> { + // No history means to create a first version and a follow-up with the copied values + final ConfigurationRecord newFirstVersion = new ConfigurationRecord( + null, + configRec.getInstitutionId(), + newNodeRec.getId(), + ConfigurationDAOBatchService.INITIAL_VERSION_NAME, + DateTime.now(DateTimeZone.UTC), + BooleanUtils.toInteger(false)); + this.batchConfigurationRecordMapper.insert(newFirstVersion); + this.batchSqlSessionTemplate.flushStatements(); + this.copyValues( + configRec.getInstitutionId(), + configRec.getId(), + newFirstVersion.getId()); + // and copy the follow-up + final ConfigurationRecord followup = new ConfigurationRecord( + null, + configRec.getInstitutionId(), + newNodeRec.getId(), + null, + null, + BooleanUtils.toInteger(true)); + this.batchConfigurationRecordMapper.insert(followup); + this.batchSqlSessionTemplate.flushStatements(); + this.copyValues( + configRec.getInstitutionId(), + configRec.getId(), + followup.getId()); + }); + } + + this.batchSqlSessionTemplate.flushStatements(); + return newNodeRec; + } + + private Result copyConfiguration( + final Long institutionId, + final Long fromConfigurationId, + final Long toConfigurationNodeId) { + + return Result.tryCatch(() -> { + final ConfigurationRecord fromRecord = this.batchConfigurationRecordMapper + .selectByPrimaryKey(fromConfigurationId); + + if (!fromRecord.getInstitutionId().equals(institutionId)) { + throw new IllegalArgumentException("Institution integrity violation"); + } + + final ConfigurationRecord configurationRecord = new ConfigurationRecord( + null, + fromRecord.getInstitutionId(), + toConfigurationNodeId, + fromRecord.getVersion(), + fromRecord.getVersionDate(), + fromRecord.getFollowup()); + this.batchConfigurationRecordMapper.insert(configurationRecord); + this.batchSqlSessionTemplate.flushStatements(); + return configurationRecord; + }) + .flatMap(ConfigurationDAOImpl::toDomainModel) + .map(newConfig -> { + this.copyValues( + institutionId, + fromConfigurationId, + newConfig.getId()); + return newConfig; + }); + } + + private void copyValues( + final Long institutionId, + final Long fromConfigId, + final Long toConfigId) { + + this.batchConfigurationValueRecordMapper + .selectByExample() + .where( + ConfigurationValueRecordDynamicSqlSupport.institutionId, + isEqualTo(institutionId)) + .and( + ConfigurationValueRecordDynamicSqlSupport.configurationId, + isEqualTo(fromConfigId)) + .build() + .execute() + .stream() + .map(fromRec -> new ConfigurationValueRecord( + null, + fromRec.getInstitutionId(), + toConfigId, + fromRec.getConfigurationAttributeId(), + fromRec.getListIndex(), + fromRec.getValue())) + .forEach(this.batchConfigurationValueRecordMapper::insert); + } + + private ConfigurationRecord getFollowupConfigurationRecord(final Long configurationNodeId) { + return this.batchConfigurationRecordMapper + .selectByExample() + .where( + ConfigurationRecordDynamicSqlSupport.configurationNodeId, + isEqualTo(configurationNodeId)) + .and( + ConfigurationRecordDynamicSqlSupport.followup, + isEqualTo(BooleanUtils.toInteger(true))) + .build() + .execute() + .stream() + .collect(Utils.toSingleton()); + } + + private void saveAsTable( + final ConfigurationTableValues value, + final ConfigurationAttributeRecord attributeRecord) { + + final Map attributeMap = + this.batchConfigurationAttributeRecordMapper + .selectByExample() + .where( + ConfigurationAttributeRecordDynamicSqlSupport.parentId, + isEqualTo(attributeRecord.getId())) + .build() + .execute() + .stream() + .collect(Collectors.toMap(ConfigurationAttributeRecord::getId, Function.identity())); + + final List columnAttributeIds = attributeMap.values() + .stream() + .map(ConfigurationAttributeRecord::getId) + .collect(Collectors.toList()); + + // first delete all old values of this table + this.batchConfigurationValueRecordMapper.deleteByExample() + .where( + ConfigurationValueRecordDynamicSqlSupport.configurationId, + isEqualTo(value.configurationId)) + .and( + ConfigurationValueRecordDynamicSqlSupport.configurationAttributeId, + SqlBuilder.isIn(columnAttributeIds)) + .build() + .execute(); + + // then add the new values + for (final TableValue tableValue : value.values) { + final ConfigurationAttributeRecord columnAttr = attributeMap.get(tableValue.attributeId); + final ConfigurationValueRecord valueRecord = new ConfigurationValueRecord( + null, + value.institutionId, + value.configurationId, + columnAttr.getId(), + tableValue.listIndex, + tableValue.value); + + this.batchConfigurationValueRecordMapper.insertSelective(valueRecord); + } + } + + private void saveAsComposite(final ConfigurationTableValues value) { + for (final TableValue tableValue : value.values) { + + final List valuePK = this.batchConfigurationValueRecordMapper.selectIdsByExample() + .where( + ConfigurationValueRecordDynamicSqlSupport.configurationId, + isEqualTo(value.configurationId)) + .and( + ConfigurationValueRecordDynamicSqlSupport.configurationAttributeId, + isEqualTo(tableValue.attributeId)) + .and( + ConfigurationValueRecordDynamicSqlSupport.listIndex, + isEqualTo(tableValue.listIndex)) + .build() + .execute(); + + if (valuePK != null && valuePK.size() > 1) { + throw new IllegalStateException("Expected no more then one element"); + } + + if (valuePK == null || valuePK.isEmpty()) { + // insert + this.batchConfigurationValueRecordMapper.insert( + new ConfigurationValueRecord( + null, + value.institutionId, + value.configurationId, + tableValue.attributeId, + tableValue.listIndex, + tableValue.value)); + } else { + // update + this.batchConfigurationValueRecordMapper.updateByPrimaryKey( + new ConfigurationValueRecord( + valuePK.iterator().next(), + value.institutionId, + value.configurationId, + tableValue.attributeId, + tableValue.listIndex, + tableValue.value)); + } + } + } + + private Result checkInstitutionalIntegrity(final ConfigurationTableValues data) { + return Result.tryCatch(() -> { + final ConfigurationRecord r = this.batchConfigurationRecordMapper.selectByPrimaryKey(data.configurationId); + if (r.getInstitutionId().longValue() != data.institutionId.longValue()) { + throw new IllegalArgumentException("Institutional integrity constraint violation"); + } + return data; + }); + } + + private ConfigurationTableValues checkFollowUpIntegrity(final ConfigurationTableValues data) { + checkFollowUp(data.configurationId); + return data; + } + + private void checkFollowUp(final Long configurationId) { + final ConfigurationRecord config = this.batchConfigurationRecordMapper + .selectByPrimaryKey(configurationId); + + if (!BooleanUtils.toBoolean(config.getFollowup())) { + throw new IllegalArgumentException( + "Forbidden to modify an configuration value of a none follow-up configuration"); + } + } + + /* + * Creates the first Configuration and a follow-up within a newly created ConfigurationNode. + * This creates a first new Configuration and all attribute values for that either + * from the default values or from template values if defined. + * Then a follow-up Configuration is created with the same values to follow-up user input + */ + private Result createInitialConfiguration(final ConfigurationNode config) { + return Result.tryCatch(() -> { + + final ConfigurationRecord initConfig = new ConfigurationRecord( + null, + config.institutionId, + config.id, + INITIAL_VERSION_NAME, + DateTime.now(DateTimeZone.UTC), + BooleanUtils.toInteger(false)); + + this.batchConfigurationRecordMapper.insert(initConfig); + this.batchSqlSessionTemplate.flushStatements(); + + createAttributeValues(config, initConfig) + .getOrThrow(); + + final ConfigurationRecord followup = new ConfigurationRecord( + null, + config.institutionId, + config.id, + null, + null, + BooleanUtils.toInteger(true)); + + this.batchConfigurationRecordMapper.insert(followup); + this.batchSqlSessionTemplate.flushStatements(); + + this.copyValues( + config.institutionId, + initConfig.getId(), + followup.getId()); + + return config; + }); + } + + /* + * Creates all attribute values for a given ConfigurationNode with its newly created first ConfigurationRecord + * If the ConfigurationNode has a templateId this will gather all attributes values from the latest + * configuration of this ConfigurationNode template to override the default values. + * Otherwise creates all attribute values from the default values. + */ + private Result createAttributeValues( + final ConfigurationNode configNode, + final ConfigurationRecord config) { + + return Result.tryCatch(() -> { + + // go through all configuration attributes and create and store the default value + this.batchConfigurationAttributeRecordMapper + .selectByExample() + .build() + .execute() + .stream() + // filter child attributes of tables. No default value for tables. Use templates for that + .filter(ConfigurationDAOBatchService::filterChildAttribute) + .forEach(attrRec -> this.batchConfigurationValueRecordMapper.insert(new ConfigurationValueRecord( + null, + configNode.institutionId, + config.getId(), + attrRec.getId(), + 0, + attrRec.getDefaultValue()))); + + // override with template values if available + if (configNode.templateId == null || configNode.templateId.equals(ConfigurationNode.DEFAULT_TEMPLATE_ID)) { + initAdditionalDefaultValues(configNode, config); + } else { + writeTemplateValues(configNode, config); + } + + return configNode; + }); + } + + private void initAdditionalDefaultValues( + final ConfigurationNode configNode, + final ConfigurationRecord config) { + + // get all attributes and map the names to id's + final Map attributeMap = this.batchConfigurationAttributeRecordMapper + .selectByExample() + .build() + .execute() + .stream() + .map(ConfigurationAttributeDAOImpl::toDomainModel) + .map(Result::get) + .filter(Objects::nonNull) + .collect(Collectors.toMap( + attr -> attr.name, + Function.identity())); + + this.examConfigInitService.getAdditionalDefaultValues( + configNode.institutionId, + config.getId(), + attributeMap::get) + .forEach(value -> { + final ConfigurationValueRecord valueRec = new ConfigurationValueRecord( + null, + value.institutionId, + value.configurationId, + value.attributeId, + value.listIndex, + value.value); + + this.batchConfigurationValueRecordMapper.insert(valueRec); + }); + + this.batchSqlSessionTemplate.flushStatements(); + } + + private void writeTemplateValues( + final ConfigurationNode configNode, + final ConfigurationRecord config) { + + final List templateValues = getTemplateValues(configNode); + templateValues.forEach(templateValue -> { + final Long existingId = this.batchConfigurationValueRecordMapper + .selectIdsByExample() + .where( + ConfigurationValueRecordDynamicSqlSupport.configurationId, + isEqualTo(config.getId())) + .and( + ConfigurationValueRecordDynamicSqlSupport.configurationAttributeId, + isEqualTo(templateValue.getConfigurationAttributeId())) + .and( + ConfigurationValueRecordDynamicSqlSupport.listIndex, + isEqualTo(templateValue.getListIndex())) + .build() + .execute() + .stream() + .findFirst() + .orElse(null); + + final ConfigurationValueRecord valueRec = new ConfigurationValueRecord( + existingId, + configNode.institutionId, + config.getId(), + templateValue.getConfigurationAttributeId(), + templateValue.getListIndex(), + templateValue.getValue()); + + if (existingId != null) { + this.batchConfigurationValueRecordMapper.updateByPrimaryKey(valueRec); + } else { + this.batchConfigurationValueRecordMapper.insert(valueRec); + } + }); + + this.batchSqlSessionTemplate.flushStatements(); + } + + private static boolean filterChildAttribute(final ConfigurationAttributeRecord rec) { + + if (rec.getParentId() == null) { + return true; + } + + return BooleanUtils.toBoolean(ConfigurationAttribute.getDependencyValue( + ConfigurationAttribute.DEPENDENCY_CREATE_DEFAULT_VALUE, + rec.getDependencies())); + } + + /* + * Get values from template with configuration attribute id mapped to the value + * returns empty list if no template available + */ + private List getTemplateValues(final ConfigurationNode configNode) { + if (configNode.templateId == null || configNode.templateId.equals(ConfigurationNode.DEFAULT_TEMPLATE_ID)) { + return Collections.emptyList(); + } + + final Long configurationId = this.batchConfigurationRecordMapper.selectByExample() + .where(ConfigurationRecordDynamicSqlSupport.configurationNodeId, isEqualTo(configNode.templateId)) + .and(ConfigurationRecordDynamicSqlSupport.followup, isEqualTo(BooleanUtils.toIntegerObject(true))) + .build() + .execute() + .stream() + .collect(Utils.toSingleton()) + .getId(); + + return this.batchConfigurationValueRecordMapper.selectByExample() + .where(ConfigurationValueRecordDynamicSqlSupport.configurationId, isEqualTo(configurationId)) + .build() + .execute(); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamConfigurationMapDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamConfigurationMapDAOImpl.java index e3a8685e..f6e70e47 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamConfigurationMapDAOImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamConfigurationMapDAOImpl.java @@ -1,494 +1,480 @@ -/* - * 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.dao.impl; - -import static org.mybatis.dynamic.sql.SqlBuilder.*; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Set; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.stream.Collectors; - -import org.apache.commons.lang3.StringUtils; -import org.mybatis.dynamic.sql.SqlBuilder; -import org.springframework.context.annotation.Lazy; -import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; - -import ch.ethz.seb.sebserver.gbl.api.API.BulkActionType; -import ch.ethz.seb.sebserver.gbl.api.APIMessage.APIMessageException; -import ch.ethz.seb.sebserver.gbl.api.APIMessage.ErrorMessage; -import ch.ethz.seb.sebserver.gbl.api.EntityType; -import ch.ethz.seb.sebserver.gbl.model.EntityKey; -import ch.ethz.seb.sebserver.gbl.model.exam.Exam; -import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamType; -import ch.ethz.seb.sebserver.gbl.model.exam.ExamConfigurationMap; -import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode.ConfigurationStatus; -import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; -import ch.ethz.seb.sebserver.gbl.util.Result; -import ch.ethz.seb.sebserver.gbl.util.Utils; -import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ConfigurationNodeRecordDynamicSqlSupport; -import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ConfigurationNodeRecordMapper; -import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ConfigurationRecordDynamicSqlSupport; -import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ExamConfigurationMapRecordDynamicSqlSupport; -import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ExamConfigurationMapRecordMapper; -import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ExamRecordDynamicSqlSupport; -import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ExamRecordMapper; -import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ConfigurationNodeRecord; -import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ExamConfigurationMapRecord; -import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ExamRecord; -import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.impl.BulkAction; -import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentialService; -import ch.ethz.seb.sebserver.webservice.servicelayer.dao.DAOLoggingSupport; -import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamConfigurationMapDAO; -import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO; -import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; -import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ResourceNotFoundException; -import ch.ethz.seb.sebserver.webservice.servicelayer.dao.TransactionHandler; - -@Lazy -@Component -@WebServiceProfile -public class ExamConfigurationMapDAOImpl implements ExamConfigurationMapDAO { - - private final ExamRecordMapper examRecordMapper; - private final ExamConfigurationMapRecordMapper examConfigurationMapRecordMapper; - private final ConfigurationNodeRecordMapper configurationNodeRecordMapper; - private final ClientCredentialService clientCredentialService; - private final ExamDAO examDAO; - - protected ExamConfigurationMapDAOImpl( - final ExamRecordMapper examRecordMapper, - final ExamConfigurationMapRecordMapper examConfigurationMapRecordMapper, - final ConfigurationNodeRecordMapper configurationNodeRecordMapper, - final ClientCredentialService clientCredentialService, - final ExamDAO examDAO) { - - this.examRecordMapper = examRecordMapper; - this.examConfigurationMapRecordMapper = examConfigurationMapRecordMapper; - this.configurationNodeRecordMapper = configurationNodeRecordMapper; - this.clientCredentialService = clientCredentialService; - this.examDAO = examDAO; - } - - @Override - public EntityType entityType() { - return EntityType.EXAM_CONFIGURATION_MAP; - } - - @Override - @Transactional(readOnly = true) - public Result byPK(final Long id) { - return recordById(id) - .flatMap(this::toDomainModel); - } - - @Override - @Transactional(readOnly = true) - public Result> allOf(final Set pks) { - return Result.tryCatch(() -> { - return this.examConfigurationMapRecordMapper.selectByExample() - .where(ExamConfigurationMapRecordDynamicSqlSupport.id, isIn(new ArrayList<>(pks))) - .build() - .execute() - .stream() - .map(this::toDomainModel) - .flatMap(DAOLoggingSupport::logAndSkipOnError) - .collect(Collectors.toList()); - }); - } - - @Override - @Transactional(readOnly = true) - public Result> allMatching( - final FilterMap filterMap, - final Predicate predicate) { - - return Result.tryCatch(() -> this.examConfigurationMapRecordMapper - .selectByExample() - .where( - ExamConfigurationMapRecordDynamicSqlSupport.institutionId, - SqlBuilder.isEqualToWhenPresent(filterMap.getInstitutionId())) - .and( - ExamConfigurationMapRecordDynamicSqlSupport.examId, - SqlBuilder.isEqualToWhenPresent(filterMap.getExamConfigExamId())) - .and( - ExamConfigurationMapRecordDynamicSqlSupport.configurationNodeId, - SqlBuilder.isEqualToWhenPresent(filterMap.getExamConfigConfigId())) - .build() - .execute() - .stream() - .map(this::toDomainModel) - .flatMap(DAOLoggingSupport::logAndSkipOnError) - .filter(predicate) - .collect(Collectors.toList())); - } - - @Override - @Transactional(readOnly = true) - public Result byMapping(final Long examId, final Long configurationNodeId) { - return Result.tryCatch(() -> this.examConfigurationMapRecordMapper - .selectByExample() - .where( - ExamConfigurationMapRecordDynamicSqlSupport.examId, - SqlBuilder.isEqualTo(examId)) - .and( - ExamConfigurationMapRecordDynamicSqlSupport.configurationNodeId, - SqlBuilder.isEqualTo(configurationNodeId)) - .build() - .execute() - .stream() - .map(this::toDomainModel) - .flatMap(DAOLoggingSupport::logAndSkipOnError) - .collect(Utils.toSingleton())); - } - - @Override - @Transactional(readOnly = true) - public Result getConfigPasswortCipher(final Long examId, final Long configurationNodeId) { - return Result.tryCatch(() -> this.examConfigurationMapRecordMapper - .selectByExample() - .where( - ExamConfigurationMapRecordDynamicSqlSupport.examId, - SqlBuilder.isEqualTo(examId)) - .and( - ExamConfigurationMapRecordDynamicSqlSupport.configurationNodeId, - SqlBuilder.isEqualTo(configurationNodeId)) - .build() - .execute() - .stream() - .collect(Utils.toSingleton())) - .map(ExamConfigurationMapRecord::getEncryptSecret); - } - - @Override - @Transactional(readOnly = true) - public Result getDefaultConfigurationNode(final Long examId) { - return Result.tryCatch(() -> this.examConfigurationMapRecordMapper - .selectByExample() - .where( - ExamConfigurationMapRecordDynamicSqlSupport.examId, - SqlBuilder.isEqualTo(examId)) - .and( - ExamConfigurationMapRecordDynamicSqlSupport.userNames, - SqlBuilder.isNull()) - .build() - .execute() - .stream() - .map(mapping -> mapping.getConfigurationNodeId()) - .collect(Utils.toSingleton())); - } - - @Override - @Transactional(readOnly = true) - public Result getUserConfigurationNodeId(final Long examId, final String userId) { - return Result.tryCatch(() -> this.examConfigurationMapRecordMapper - .selectByExample() - .where( - ExamConfigurationMapRecordDynamicSqlSupport.examId, - SqlBuilder.isEqualTo(examId)) - .and( - ExamConfigurationMapRecordDynamicSqlSupport.userNames, - SqlBuilder.isLike(Utils.toSQLWildcard(userId))) - .build() - .execute() - .stream() - .map(mapping -> mapping.getConfigurationNodeId()) - .collect(Utils.toSingleton())); - } - - @Override - @Transactional(readOnly = true) - public Result> getConfigurationNodeIds(final Long examId) { - return Result.tryCatch(() -> this.examConfigurationMapRecordMapper - .selectByExample() - .where( - ExamConfigurationMapRecordDynamicSqlSupport.examId, - SqlBuilder.isEqualTo(examId)) - .build() - .execute() - .stream() - .map(mapping -> mapping.getConfigurationNodeId()) - .collect(Collectors.toList())); - } - - @Override - @Transactional - public Result createNew(final ExamConfigurationMap data) { - return checkMappingIntegrity(data) - .map(config -> { - final ExamConfigurationMapRecord newRecord = new ExamConfigurationMapRecord( - null, - data.institutionId, - data.examId, - data.configurationNodeId, - data.userNames, - getEncryptionPassword(data)); - - this.examConfigurationMapRecordMapper.insert(newRecord); - return newRecord; - }) - .flatMap(this::toDomainModel) - .onError(TransactionHandler::rollback); - } - - @Override - @Transactional - public Result save(final ExamConfigurationMap data) { - return Result.tryCatch(() -> { - - final ExamConfigurationMapRecord newRecord = new ExamConfigurationMapRecord( - data.id, - null, - null, - null, - data.userNames, - getEncryptionPassword(data)); - - this.examConfigurationMapRecordMapper.updateByPrimaryKeySelective(newRecord); - return this.examConfigurationMapRecordMapper.selectByPrimaryKey(data.id); - }) - .flatMap(this::toDomainModel) - .onError(TransactionHandler::rollback); - } - - @Override - @Transactional - public Result> delete(final Set all) { - return Result.tryCatch(() -> { - - final List ids = extractListOfPKs(all); - - this.examConfigurationMapRecordMapper.deleteByExample() - .where(ExamConfigurationMapRecordDynamicSqlSupport.id, isIn(ids)) - .build() - .execute(); - - return ids.stream() - .map(id -> new EntityKey(id, EntityType.EXAM_CONFIGURATION_MAP)) - .collect(Collectors.toList()); - }); - } - - @Override - @Transactional(readOnly = true) - public Set getDependencies(final BulkAction bulkAction) { - if (bulkAction.type == BulkActionType.ACTIVATE || bulkAction.type == BulkActionType.DEACTIVATE) { - return Collections.emptySet(); - } - - // define the select function in case of source type - Function>> selectionFunction; - switch (bulkAction.sourceType) { - case INSTITUTION: - selectionFunction = this::allIdsOfInstitution; - break; - case LMS_SETUP: - selectionFunction = this::allIdsOfLmsSetup; - break; - case EXAM: - selectionFunction = this::allIdsOfExam; - break; - case CONFIGURATION_NODE: - selectionFunction = this::allIdsOfConfig; - break; - default: - selectionFunction = key -> Result.of(Collections.emptyList()); //empty select function - break; - } - - return getDependencies(bulkAction, selectionFunction); - } - - @Override - @Transactional(readOnly = true) - public Result> getExamIdsForConfigNodeId(final Long configurationNodeId) { - return Result.tryCatch(() -> { - return this.examConfigurationMapRecordMapper.selectByExample() - .where( - ExamConfigurationMapRecordDynamicSqlSupport.configurationNodeId, - isEqualTo(configurationNodeId)) - .build() - .execute() - .stream() - .map(record -> record.getExamId()) - .collect(Collectors.toList()); - }); - } - - @Override - @Transactional(readOnly = true) - public Result> getExamIdsForConfigId(final Long configurationId) { - return Result.tryCatch(() -> { - return this.configurationNodeRecordMapper.selectIdsByExample() - .leftJoin(ConfigurationRecordDynamicSqlSupport.configurationRecord) - .on( - ConfigurationRecordDynamicSqlSupport.configurationNodeId, - equalTo(ConfigurationNodeRecordDynamicSqlSupport.id)) - .where( - ConfigurationRecordDynamicSqlSupport.id, - isEqualTo(configurationId)) - .build() - .execute() - .stream() - .collect(Utils.toSingleton()); - }) - .flatMap(this::getExamIdsForConfigNodeId); - } - - private Result recordById(final Long id) { - return Result.tryCatch(() -> { - final ExamConfigurationMapRecord record = this.examConfigurationMapRecordMapper - .selectByPrimaryKey(id); - if (record == null) { - throw new ResourceNotFoundException( - EntityType.EXAM_CONFIGURATION_MAP, - String.valueOf(id)); - } - return record; - }); - } - - private Result toDomainModel(final ExamConfigurationMapRecord record) { - return Result.tryCatch(() -> { - - final ConfigurationNodeRecord config = this.configurationNodeRecordMapper - .selectByPrimaryKey(record.getConfigurationNodeId()); - final String status = config.getStatus(); - - final Exam exam = this.examDAO.byPK(record.getExamId()) - .getOr(null); - - return new ExamConfigurationMap( - record.getId(), - record.getInstitutionId(), - record.getExamId(), - (exam != null) ? exam.name : null, - (exam != null) ? exam.description : null, - (exam != null) ? exam.startTime : null, - (exam != null) ? exam.type : ExamType.UNDEFINED, - record.getConfigurationNodeId(), - record.getUserNames(), - null, - null, - config.getName(), - config.getDescription(), - (StringUtils.isNotBlank(status)) ? ConfigurationStatus.valueOf(status) : null); - }); - } - - private Result checkMappingIntegrity(final ExamConfigurationMap data) { - return Result.tryCatch(() -> { - final ConfigurationNodeRecord config = - this.configurationNodeRecordMapper.selectByPrimaryKey(data.configurationNodeId); - - if (config == null) { - throw new ResourceNotFoundException( - EntityType.CONFIGURATION_NODE, - String.valueOf(data.configurationNodeId)); - } - - if (config.getInstitutionId().longValue() != data.institutionId.longValue()) { - throw new IllegalArgumentException("Institutional integrity constraint violation"); - } - - final ExamRecord exam = this.examRecordMapper.selectByPrimaryKey(data.examId); - - if (exam == null) { - throw new ResourceNotFoundException( - EntityType.EXAM, - String.valueOf(data.configurationNodeId)); - } - - if (exam.getInstitutionId().longValue() != data.institutionId.longValue()) { - throw new IllegalArgumentException("Institutional integrity constraint violation"); - } - - return data; - }); - } - - private Result> allIdsOfInstitution(final EntityKey institutionKey) { - return Result.tryCatch(() -> { - return this.examConfigurationMapRecordMapper.selectIdsByExample() - .where( - ExamConfigurationMapRecordDynamicSqlSupport.institutionId, - isEqualTo(Long.valueOf(institutionKey.modelId))) - .build() - .execute() - .stream() - .map(id -> new EntityKey(id, EntityType.EXAM_CONFIGURATION_MAP)) - .collect(Collectors.toList()); - }); - } - - private Result> allIdsOfLmsSetup(final EntityKey lmsSetupKey) { - return Result.tryCatch(() -> { - return this.examConfigurationMapRecordMapper.selectIdsByExample() - .leftJoin(ExamRecordDynamicSqlSupport.examRecord) - .on( - ExamRecordDynamicSqlSupport.id, - equalTo(ExamConfigurationMapRecordDynamicSqlSupport.examId)) - - .where( - ExamRecordDynamicSqlSupport.lmsSetupId, - isEqualTo(Long.valueOf(lmsSetupKey.modelId))) - .build() - .execute() - .stream() - .map(id -> new EntityKey(id, EntityType.EXAM_CONFIGURATION_MAP)) - .collect(Collectors.toList()); - }); - } - - private Result> allIdsOfExam(final EntityKey examKey) { - return Result.tryCatch(() -> { - return this.examConfigurationMapRecordMapper.selectIdsByExample() - .where( - ExamConfigurationMapRecordDynamicSqlSupport.examId, - isEqualTo(Long.valueOf(examKey.modelId))) - .build() - .execute() - .stream() - .map(id -> new EntityKey(id, EntityType.EXAM_CONFIGURATION_MAP)) - .collect(Collectors.toList()); - }); - } - - private Result> allIdsOfConfig(final EntityKey configKey) { - return Result.tryCatch(() -> { - return this.examConfigurationMapRecordMapper.selectIdsByExample() - .where( - ExamConfigurationMapRecordDynamicSqlSupport.configurationNodeId, - isEqualTo(Long.valueOf(configKey.modelId))) - .build() - .execute() - .stream() - .map(id -> new EntityKey(id, EntityType.EXAM_CONFIGURATION_MAP)) - .collect(Collectors.toList()); - }); - } - - private String getEncryptionPassword(final ExamConfigurationMap examConfigurationMap) { - if (examConfigurationMap.hasEncryptionSecret() && - !examConfigurationMap.encryptSecret.equals(examConfigurationMap.confirmEncryptSecret)) { - throw new APIMessageException(ErrorMessage.PASSWORD_MISMATCH); - } - - final CharSequence encrypted_encrypt_secret = examConfigurationMap.hasEncryptionSecret() - ? this.clientCredentialService.encrypt(examConfigurationMap.encryptSecret) - : null; - return (encrypted_encrypt_secret != null) ? encrypted_encrypt_secret.toString() : null; - } - -} +/* + * 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.dao.impl; + +import static org.mybatis.dynamic.sql.SqlBuilder.*; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.StringUtils; +import org.mybatis.dynamic.sql.SqlBuilder; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import ch.ethz.seb.sebserver.gbl.api.API.BulkActionType; +import ch.ethz.seb.sebserver.gbl.api.APIMessage.APIMessageException; +import ch.ethz.seb.sebserver.gbl.api.APIMessage.ErrorMessage; +import ch.ethz.seb.sebserver.gbl.api.EntityType; +import ch.ethz.seb.sebserver.gbl.model.EntityKey; +import ch.ethz.seb.sebserver.gbl.model.exam.Exam; +import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamType; +import ch.ethz.seb.sebserver.gbl.model.exam.ExamConfigurationMap; +import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode.ConfigurationStatus; +import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; +import ch.ethz.seb.sebserver.gbl.util.Result; +import ch.ethz.seb.sebserver.gbl.util.Utils; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ConfigurationNodeRecordDynamicSqlSupport; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ConfigurationNodeRecordMapper; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ConfigurationRecordDynamicSqlSupport; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ExamConfigurationMapRecordDynamicSqlSupport; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ExamConfigurationMapRecordMapper; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ExamRecordDynamicSqlSupport; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ExamRecordMapper; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ConfigurationNodeRecord; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ExamConfigurationMapRecord; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ExamRecord; +import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.impl.BulkAction; +import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentialService; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.DAOLoggingSupport; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamConfigurationMapDAO; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ResourceNotFoundException; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.TransactionHandler; + +@Lazy +@Component +@WebServiceProfile +public class ExamConfigurationMapDAOImpl implements ExamConfigurationMapDAO { + + private final ExamRecordMapper examRecordMapper; + private final ExamConfigurationMapRecordMapper examConfigurationMapRecordMapper; + private final ConfigurationNodeRecordMapper configurationNodeRecordMapper; + private final ClientCredentialService clientCredentialService; + private final ExamDAO examDAO; + + protected ExamConfigurationMapDAOImpl( + final ExamRecordMapper examRecordMapper, + final ExamConfigurationMapRecordMapper examConfigurationMapRecordMapper, + final ConfigurationNodeRecordMapper configurationNodeRecordMapper, + final ClientCredentialService clientCredentialService, + final ExamDAO examDAO) { + + this.examRecordMapper = examRecordMapper; + this.examConfigurationMapRecordMapper = examConfigurationMapRecordMapper; + this.configurationNodeRecordMapper = configurationNodeRecordMapper; + this.clientCredentialService = clientCredentialService; + this.examDAO = examDAO; + } + + @Override + public EntityType entityType() { + return EntityType.EXAM_CONFIGURATION_MAP; + } + + @Override + @Transactional(readOnly = true) + public Result byPK(final Long id) { + return recordById(id) + .flatMap(this::toDomainModel); + } + + @Override + @Transactional(readOnly = true) + public Result> allOf(final Set pks) { + return Result.tryCatch(() -> this.examConfigurationMapRecordMapper.selectByExample() + .where(ExamConfigurationMapRecordDynamicSqlSupport.id, isIn(new ArrayList<>(pks))) + .build() + .execute() + .stream() + .map(this::toDomainModel) + .flatMap(DAOLoggingSupport::logAndSkipOnError) + .collect(Collectors.toList())); + } + + @Override + @Transactional(readOnly = true) + public Result> allMatching( + final FilterMap filterMap, + final Predicate predicate) { + + return Result.tryCatch(() -> this.examConfigurationMapRecordMapper + .selectByExample() + .where( + ExamConfigurationMapRecordDynamicSqlSupport.institutionId, + SqlBuilder.isEqualToWhenPresent(filterMap.getInstitutionId())) + .and( + ExamConfigurationMapRecordDynamicSqlSupport.examId, + SqlBuilder.isEqualToWhenPresent(filterMap.getExamConfigExamId())) + .and( + ExamConfigurationMapRecordDynamicSqlSupport.configurationNodeId, + SqlBuilder.isEqualToWhenPresent(filterMap.getExamConfigConfigId())) + .build() + .execute() + .stream() + .map(this::toDomainModel) + .flatMap(DAOLoggingSupport::logAndSkipOnError) + .filter(predicate) + .collect(Collectors.toList())); + } + + @Override + @Transactional(readOnly = true) + public Result byMapping(final Long examId, final Long configurationNodeId) { + return Result.tryCatch(() -> this.examConfigurationMapRecordMapper + .selectByExample() + .where( + ExamConfigurationMapRecordDynamicSqlSupport.examId, + SqlBuilder.isEqualTo(examId)) + .and( + ExamConfigurationMapRecordDynamicSqlSupport.configurationNodeId, + SqlBuilder.isEqualTo(configurationNodeId)) + .build() + .execute() + .stream() + .map(this::toDomainModel) + .flatMap(DAOLoggingSupport::logAndSkipOnError) + .collect(Utils.toSingleton())); + } + + @Override + @Transactional(readOnly = true) + public Result getConfigPasswordCipher(final Long examId, final Long configurationNodeId) { + return Result.tryCatch(() -> this.examConfigurationMapRecordMapper + .selectByExample() + .where( + ExamConfigurationMapRecordDynamicSqlSupport.examId, + SqlBuilder.isEqualTo(examId)) + .and( + ExamConfigurationMapRecordDynamicSqlSupport.configurationNodeId, + SqlBuilder.isEqualTo(configurationNodeId)) + .build() + .execute() + .stream() + .collect(Utils.toSingleton())) + .map(ExamConfigurationMapRecord::getEncryptSecret); + } + + @Override + @Transactional(readOnly = true) + public Result getDefaultConfigurationNode(final Long examId) { + return Result.tryCatch(() -> this.examConfigurationMapRecordMapper + .selectByExample() + .where( + ExamConfigurationMapRecordDynamicSqlSupport.examId, + SqlBuilder.isEqualTo(examId)) + .and( + ExamConfigurationMapRecordDynamicSqlSupport.userNames, + SqlBuilder.isNull()) + .build() + .execute() + .stream() + .map(ExamConfigurationMapRecord::getConfigurationNodeId) + .collect(Utils.toSingleton())); + } + + @Override + @Transactional(readOnly = true) + public Result getUserConfigurationNodeId(final Long examId, final String userId) { + return Result.tryCatch(() -> this.examConfigurationMapRecordMapper + .selectByExample() + .where( + ExamConfigurationMapRecordDynamicSqlSupport.examId, + SqlBuilder.isEqualTo(examId)) + .and( + ExamConfigurationMapRecordDynamicSqlSupport.userNames, + SqlBuilder.isLike(Utils.toSQLWildcard(userId))) + .build() + .execute() + .stream() + .map(ExamConfigurationMapRecord::getConfigurationNodeId) + .collect(Utils.toSingleton())); + } + + @Override + @Transactional(readOnly = true) + public Result> getConfigurationNodeIds(final Long examId) { + return Result.tryCatch(() -> this.examConfigurationMapRecordMapper + .selectByExample() + .where( + ExamConfigurationMapRecordDynamicSqlSupport.examId, + SqlBuilder.isEqualTo(examId)) + .build() + .execute() + .stream() + .map(ExamConfigurationMapRecord::getConfigurationNodeId) + .collect(Collectors.toList())); + } + + @Override + @Transactional + public Result createNew(final ExamConfigurationMap data) { + return checkMappingIntegrity(data) + .map(config -> { + final ExamConfigurationMapRecord newRecord = new ExamConfigurationMapRecord( + null, + data.institutionId, + data.examId, + data.configurationNodeId, + data.userNames, + getEncryptionPassword(data)); + + this.examConfigurationMapRecordMapper.insert(newRecord); + return newRecord; + }) + .flatMap(this::toDomainModel) + .onError(TransactionHandler::rollback); + } + + @Override + @Transactional + public Result save(final ExamConfigurationMap data) { + return Result.tryCatch(() -> { + + final ExamConfigurationMapRecord newRecord = new ExamConfigurationMapRecord( + data.id, + null, + null, + null, + data.userNames, + getEncryptionPassword(data)); + + this.examConfigurationMapRecordMapper.updateByPrimaryKeySelective(newRecord); + return this.examConfigurationMapRecordMapper.selectByPrimaryKey(data.id); + }) + .flatMap(this::toDomainModel) + .onError(TransactionHandler::rollback); + } + + @Override + @Transactional + public Result> delete(final Set all) { + return Result.tryCatch(() -> { + + final List ids = extractListOfPKs(all); + + this.examConfigurationMapRecordMapper.deleteByExample() + .where(ExamConfigurationMapRecordDynamicSqlSupport.id, isIn(ids)) + .build() + .execute(); + + return ids.stream() + .map(id -> new EntityKey(id, EntityType.EXAM_CONFIGURATION_MAP)) + .collect(Collectors.toList()); + }); + } + + @Override + @Transactional(readOnly = true) + public Set getDependencies(final BulkAction bulkAction) { + if (bulkAction.type == BulkActionType.ACTIVATE || bulkAction.type == BulkActionType.DEACTIVATE) { + return Collections.emptySet(); + } + + // define the select function in case of source type + Function>> selectionFunction; + switch (bulkAction.sourceType) { + case INSTITUTION: + selectionFunction = this::allIdsOfInstitution; + break; + case LMS_SETUP: + selectionFunction = this::allIdsOfLmsSetup; + break; + case EXAM: + selectionFunction = this::allIdsOfExam; + break; + case CONFIGURATION_NODE: + selectionFunction = this::allIdsOfConfig; + break; + default: + selectionFunction = key -> Result.of(Collections.emptyList()); //empty select function + break; + } + + return getDependencies(bulkAction, selectionFunction); + } + + @Override + @Transactional(readOnly = true) + public Result> getExamIdsForConfigNodeId(final Long configurationNodeId) { + return Result.tryCatch(() -> this.examConfigurationMapRecordMapper.selectByExample() + .where( + ExamConfigurationMapRecordDynamicSqlSupport.configurationNodeId, + isEqualTo(configurationNodeId)) + .build() + .execute() + .stream() + .map(ExamConfigurationMapRecord::getExamId) + .collect(Collectors.toList())); + } + + @Override + @Transactional(readOnly = true) + public Result> getExamIdsForConfigId(final Long configurationId) { + return Result.tryCatch(() -> this.configurationNodeRecordMapper.selectIdsByExample() + .leftJoin(ConfigurationRecordDynamicSqlSupport.configurationRecord) + .on( + ConfigurationRecordDynamicSqlSupport.configurationNodeId, + equalTo(ConfigurationNodeRecordDynamicSqlSupport.id)) + .where( + ConfigurationRecordDynamicSqlSupport.id, + isEqualTo(configurationId)) + .build() + .execute() + .stream() + .collect(Utils.toSingleton())) + .flatMap(this::getExamIdsForConfigNodeId); + } + + private Result recordById(final Long id) { + return Result.tryCatch(() -> { + final ExamConfigurationMapRecord record = this.examConfigurationMapRecordMapper + .selectByPrimaryKey(id); + if (record == null) { + throw new ResourceNotFoundException( + EntityType.EXAM_CONFIGURATION_MAP, + String.valueOf(id)); + } + return record; + }); + } + + private Result toDomainModel(final ExamConfigurationMapRecord record) { + return Result.tryCatch(() -> { + + final ConfigurationNodeRecord config = this.configurationNodeRecordMapper + .selectByPrimaryKey(record.getConfigurationNodeId()); + final String status = config.getStatus(); + + final Exam exam = this.examDAO.byPK(record.getExamId()) + .getOr(null); + + return new ExamConfigurationMap( + record.getId(), + record.getInstitutionId(), + record.getExamId(), + (exam != null) ? exam.name : null, + (exam != null) ? exam.description : null, + (exam != null) ? exam.startTime : null, + (exam != null) ? exam.type : ExamType.UNDEFINED, + record.getConfigurationNodeId(), + record.getUserNames(), + record.getEncryptSecret(), + null, + config.getName(), + config.getDescription(), + (StringUtils.isNotBlank(status)) ? ConfigurationStatus.valueOf(status) : null); + }); + } + + private Result checkMappingIntegrity(final ExamConfigurationMap data) { + return Result.tryCatch(() -> { + final ConfigurationNodeRecord config = + this.configurationNodeRecordMapper.selectByPrimaryKey(data.configurationNodeId); + + if (config == null) { + throw new ResourceNotFoundException( + EntityType.CONFIGURATION_NODE, + String.valueOf(data.configurationNodeId)); + } + + if (config.getInstitutionId().longValue() != data.institutionId.longValue()) { + throw new IllegalArgumentException("Institutional integrity constraint violation"); + } + + final ExamRecord exam = this.examRecordMapper.selectByPrimaryKey(data.examId); + + if (exam == null) { + throw new ResourceNotFoundException( + EntityType.EXAM, + String.valueOf(data.configurationNodeId)); + } + + if (exam.getInstitutionId().longValue() != data.institutionId.longValue()) { + throw new IllegalArgumentException("Institutional integrity constraint violation"); + } + + return data; + }); + } + + private Result> allIdsOfInstitution(final EntityKey institutionKey) { + return Result.tryCatch(() -> this.examConfigurationMapRecordMapper.selectIdsByExample() + .where( + ExamConfigurationMapRecordDynamicSqlSupport.institutionId, + isEqualTo(Long.valueOf(institutionKey.modelId))) + .build() + .execute() + .stream() + .map(id -> new EntityKey(id, EntityType.EXAM_CONFIGURATION_MAP)) + .collect(Collectors.toList())); + } + + private Result> allIdsOfLmsSetup(final EntityKey lmsSetupKey) { + return Result.tryCatch(() -> this.examConfigurationMapRecordMapper.selectIdsByExample() + .leftJoin(ExamRecordDynamicSqlSupport.examRecord) + .on( + ExamRecordDynamicSqlSupport.id, + equalTo(ExamConfigurationMapRecordDynamicSqlSupport.examId)) + + .where( + ExamRecordDynamicSqlSupport.lmsSetupId, + isEqualTo(Long.valueOf(lmsSetupKey.modelId))) + .build() + .execute() + .stream() + .map(id -> new EntityKey(id, EntityType.EXAM_CONFIGURATION_MAP)) + .collect(Collectors.toList())); + } + + private Result> allIdsOfExam(final EntityKey examKey) { + return Result.tryCatch(() -> this.examConfigurationMapRecordMapper.selectIdsByExample() + .where( + ExamConfigurationMapRecordDynamicSqlSupport.examId, + isEqualTo(Long.valueOf(examKey.modelId))) + .build() + .execute() + .stream() + .map(id -> new EntityKey(id, EntityType.EXAM_CONFIGURATION_MAP)) + .collect(Collectors.toList())); + } + + private Result> allIdsOfConfig(final EntityKey configKey) { + return Result.tryCatch(() -> this.examConfigurationMapRecordMapper.selectIdsByExample() + .where( + ExamConfigurationMapRecordDynamicSqlSupport.configurationNodeId, + isEqualTo(Long.valueOf(configKey.modelId))) + .build() + .execute() + .stream() + .map(id -> new EntityKey(id, EntityType.EXAM_CONFIGURATION_MAP)) + .collect(Collectors.toList())); + } + + private String getEncryptionPassword(final ExamConfigurationMap examConfigurationMap) { + if (examConfigurationMap.hasEncryptionSecret() && + !examConfigurationMap.encryptSecret.equals(examConfigurationMap.confirmEncryptSecret)) { + throw new APIMessageException(ErrorMessage.PASSWORD_MISMATCH); + } + + final CharSequence encrypted_encrypt_secret = examConfigurationMap.hasEncryptionSecret() + ? this.clientCredentialService.encrypt(examConfigurationMap.encryptSecret) + : null; + return (encrypted_encrypt_secret != null) ? encrypted_encrypt_secret.toString() : null; + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/SebClientConfigDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/SebClientConfigDAOImpl.java index a310aadd..a47aef74 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/SebClientConfigDAOImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/SebClientConfigDAOImpl.java @@ -1,430 +1,526 @@ -/* - * 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.dao.impl; - -import static org.mybatis.dynamic.sql.SqlBuilder.*; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Set; -import java.util.function.Predicate; -import java.util.stream.Collectors; - -import org.apache.commons.lang3.BooleanUtils; -import org.apache.commons.lang3.StringUtils; -import org.joda.time.DateTime; -import org.joda.time.DateTimeZone; -import org.springframework.context.annotation.Lazy; -import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; - -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.ErrorMessage; -import ch.ethz.seb.sebserver.gbl.api.EntityType; -import ch.ethz.seb.sebserver.gbl.model.Domain; -import ch.ethz.seb.sebserver.gbl.model.EntityKey; -import ch.ethz.seb.sebserver.gbl.model.sebconfig.SebClientConfig; -import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; -import ch.ethz.seb.sebserver.gbl.util.Result; -import ch.ethz.seb.sebserver.gbl.util.Utils; -import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.SebClientConfigRecordDynamicSqlSupport; -import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.SebClientConfigRecordMapper; -import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.AdditionalAttributeRecord; -import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.SebClientConfigRecord; -import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.impl.BulkAction; -import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentialService; -import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentials; -import ch.ethz.seb.sebserver.webservice.servicelayer.dao.DAOLoggingSupport; -import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; -import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ResourceNotFoundException; -import ch.ethz.seb.sebserver.webservice.servicelayer.dao.SebClientConfigDAO; -import ch.ethz.seb.sebserver.webservice.servicelayer.dao.TransactionHandler; - -@Lazy -@Component -@WebServiceProfile -public class SebClientConfigDAOImpl implements SebClientConfigDAO { - - private final SebClientConfigRecordMapper sebClientConfigRecordMapper; - private final ClientCredentialService clientCredentialService; - private final AdditionalAttributesDAOImpl additionalAttributesDAO; - - protected SebClientConfigDAOImpl( - final SebClientConfigRecordMapper sebClientConfigRecordMapper, - final ClientCredentialService clientCredentialService, - final AdditionalAttributesDAOImpl additionalAttributesDAO) { - - this.sebClientConfigRecordMapper = sebClientConfigRecordMapper; - this.clientCredentialService = clientCredentialService; - this.additionalAttributesDAO = additionalAttributesDAO; - } - - @Override - public EntityType entityType() { - return EntityType.SEB_CLIENT_CONFIGURATION; - } - - @Override - @Transactional(readOnly = true) - public Result byPK(final Long id) { - return recordById(id) - .flatMap(this::toDomainModel); - } - - @Override - @Transactional(readOnly = true) - public Result> all(final Long institutionId, final Boolean active) { - return Result.tryCatch(() -> { - - final List records = (active != null) - ? this.sebClientConfigRecordMapper.selectByExample() - .where( - SebClientConfigRecordDynamicSqlSupport.institutionId, - isEqualToWhenPresent(institutionId)) - .and( - SebClientConfigRecordDynamicSqlSupport.active, - isEqualToWhenPresent(BooleanUtils.toIntegerObject(active))) - .build() - .execute() - : this.sebClientConfigRecordMapper.selectByExample().build().execute(); - - return records.stream() - .map(this::toDomainModel) - .flatMap(DAOLoggingSupport::logAndSkipOnError) - .collect(Collectors.toList()); - }); - } - - @Override - @Transactional(readOnly = true) - public Result> allMatching( - final FilterMap filterMap, - final Predicate predicate) { - - return Result.tryCatch(() -> { - - return this.sebClientConfigRecordMapper - .selectByExample() - .where( - SebClientConfigRecordDynamicSqlSupport.institutionId, - isEqualToWhenPresent(filterMap.getInstitutionId())) - .and( - SebClientConfigRecordDynamicSqlSupport.name, - isLikeWhenPresent(filterMap.getName())) - .and( - SebClientConfigRecordDynamicSqlSupport.date, - isGreaterThanOrEqualToWhenPresent(filterMap.getSebClientConfigFromTime())) - .and( - SebClientConfigRecordDynamicSqlSupport.active, - isEqualToWhenPresent(filterMap.getActiveAsInt())) - .build() - .execute() - .stream() - .map(this::toDomainModel) - .flatMap(DAOLoggingSupport::logAndSkipOnError) - .filter(predicate) - .collect(Collectors.toList()); - }); - } - - @Override - public Result byClientName(final String clientName) { - return Result.tryCatch(() -> { - - return this.sebClientConfigRecordMapper - .selectByExample() - .where( - SebClientConfigRecordDynamicSqlSupport.clientName, - isEqualTo(clientName)) - .build() - .execute() - .stream() - .map(this::toDomainModel) - .flatMap(DAOLoggingSupport::logAndSkipOnError) - .collect(Utils.toSingleton()); - }); - } - - @Override - @Transactional(readOnly = true) - public Result getConfigPasswortCipherByClientName(final String clientName) { - return Result.tryCatch(() -> { - - final SebClientConfigRecord record = this.sebClientConfigRecordMapper - .selectByExample() - .where( - SebClientConfigRecordDynamicSqlSupport.clientName, - isEqualTo(clientName)) - .and( - SebClientConfigRecordDynamicSqlSupport.active, - isNotEqualTo(0)) - .build() - .execute() - .stream() - .collect(Utils.toSingleton()); - - return record.getClientSecret(); - }); - } - - @Override - @Transactional(readOnly = true) - public boolean isActive(final String modelId) { - if (StringUtils.isBlank(modelId)) { - return false; - } - - return this.sebClientConfigRecordMapper.countByExample() - .where(SebClientConfigRecordDynamicSqlSupport.id, isEqualTo(Long.valueOf(modelId))) - .and(SebClientConfigRecordDynamicSqlSupport.active, isEqualTo(BooleanUtils.toInteger(true))) - .build() - .execute() - .longValue() > 0; - } - - @Override - @Transactional - public Result> setActive(final Set all, final boolean active) { - return Result.tryCatch(() -> { - - final List ids = extractListOfPKs(all); - final SebClientConfigRecord record = new SebClientConfigRecord( - null, null, null, null, null, null, null, - BooleanUtils.toIntegerObject(active)); - - this.sebClientConfigRecordMapper.updateByExampleSelective(record) - .where(SebClientConfigRecordDynamicSqlSupport.id, isIn(ids)) - .build() - .execute(); - - return ids.stream() - .map(id -> new EntityKey(id, EntityType.SEB_CLIENT_CONFIGURATION)) - .collect(Collectors.toList()); - }); - } - - @Override - @Transactional - public Result createNew(final SebClientConfig sebClientConfig) { - return this.clientCredentialService - .generatedClientCredentials() - .map(cc -> { - - checkUniqueName(sebClientConfig); - - final SebClientConfigRecord newRecord = new SebClientConfigRecord( - null, - sebClientConfig.institutionId, - sebClientConfig.name, - DateTime.now(DateTimeZone.UTC), - cc.clientIdAsString(), - cc.secretAsString(), - getEncryptionPassword(sebClientConfig), - BooleanUtils.toInteger(BooleanUtils.isTrue(sebClientConfig.active))); - - this.sebClientConfigRecordMapper - .insert(newRecord); - - this.additionalAttributesDAO.saveAdditionalAttribute( - EntityType.SEB_CLIENT_CONFIGURATION, - newRecord.getId(), - SebClientConfig.ATTR_FALLBACK_START_URL, - sebClientConfig.fallbackStartURL); - - return newRecord; - }) - .flatMap(this::toDomainModel) - .onError(TransactionHandler::rollback); - } - - @Override - @Transactional - public Result save(final SebClientConfig sebClientConfig) { - return Result.tryCatch(() -> { - - checkUniqueName(sebClientConfig); - - final SebClientConfigRecord newRecord = new SebClientConfigRecord( - sebClientConfig.id, - null, - sebClientConfig.name, - null, - null, - null, - getEncryptionPassword(sebClientConfig), - null); - - this.sebClientConfigRecordMapper - .updateByPrimaryKeySelective(newRecord); - - this.additionalAttributesDAO.saveAdditionalAttribute( - EntityType.SEB_CLIENT_CONFIGURATION, - newRecord.getId(), - SebClientConfig.ATTR_FALLBACK_START_URL, - sebClientConfig.fallbackStartURL); - - return this.sebClientConfigRecordMapper - .selectByPrimaryKey(sebClientConfig.id); - }) - .flatMap(this::toDomainModel) - .onError(TransactionHandler::rollback); - } - - @Override - @Transactional - public Result> delete(final Set all) { - return Result.tryCatch(() -> { - - final List ids = extractListOfPKs(all); - - this.sebClientConfigRecordMapper.deleteByExample() - .where(SebClientConfigRecordDynamicSqlSupport.id, isIn(ids)) - .build() - .execute(); - - return ids.stream() - .map(id -> new EntityKey(id, EntityType.SEB_CLIENT_CONFIGURATION)) - .collect(Collectors.toList()); - }); - } - - @Override - @Transactional(readOnly = true) - public Result> allOf(final Set pks) { - return Result.tryCatch(() -> { - - return this.sebClientConfigRecordMapper.selectByExample() - .where(SebClientConfigRecordDynamicSqlSupport.id, isIn(new ArrayList<>(pks))) - .build() - .execute() - .stream() - .map(this::toDomainModel) - .flatMap(DAOLoggingSupport::logAndSkipOnError) - .collect(Collectors.toList()); - }); - } - - @Override - @Transactional(readOnly = true) - public Set getDependencies(final BulkAction bulkAction) { - // all of institution - if (bulkAction.sourceType == EntityType.INSTITUTION) { - return getDependencies(bulkAction, this::allIdsOfInstitution); - } - - return Collections.emptySet(); - } - - @Override - @Transactional(readOnly = true) - public Result getSebClientCredentials(final String modelId) { - return recordByModelId(modelId) - .map(rec -> new ClientCredentials( - rec.getClientName(), - rec.getClientSecret(), - null)); - } - - @Override - @Transactional(readOnly = true) - public Result getConfigPasswortCipher(final String modelId) { - return recordByModelId(modelId) - .map(rec -> rec.getEncryptSecret()); - } - - private Result> allIdsOfInstitution(final EntityKey institutionKey) { - return Result.tryCatch(() -> { - return this.sebClientConfigRecordMapper.selectIdsByExample() - .where(SebClientConfigRecordDynamicSqlSupport.institutionId, - isEqualTo(Long.valueOf(institutionKey.modelId))) - .build() - .execute() - .stream() - .map(id -> new EntityKey(id, EntityType.SEB_CLIENT_CONFIGURATION)) - .collect(Collectors.toList()); - }); - } - - private Result recordByModelId(final String modelId) { - return Result.tryCatch(() -> { - return recordById(Long.parseLong(modelId)).getOrThrow(); - }); - } - - private Result recordById(final Long id) { - return Result.tryCatch(() -> { - final SebClientConfigRecord record = this.sebClientConfigRecordMapper - .selectByPrimaryKey(id); - - if (record == null) { - throw new ResourceNotFoundException( - EntityType.SEB_CLIENT_CONFIGURATION, - String.valueOf(id)); - } - return record; - }); - } - - private Result toDomainModel(final SebClientConfigRecord record) { - final String fallbackURL = this.additionalAttributesDAO.getAdditionalAttributes( - EntityType.SEB_CLIENT_CONFIGURATION, - record.getId()) - .getOrThrow() - .stream() - .filter(rec -> SebClientConfig.ATTR_FALLBACK_START_URL.equals(rec.getName())) - .findFirst() - .map(AdditionalAttributeRecord::getValue) - .orElse(null); - - return Result.tryCatch(() -> new SebClientConfig( - record.getId(), - record.getInstitutionId(), - record.getName(), - fallbackURL, - record.getDate(), - null, - null, - BooleanUtils.toBooleanObject(record.getActive()))); - } - - private String getEncryptionPassword(final SebClientConfig sebClientConfig) { - if (sebClientConfig.hasEncryptionSecret() && - !sebClientConfig.encryptSecret.equals(sebClientConfig.confirmEncryptSecret)) { - throw new APIMessageException(ErrorMessage.PASSWORD_MISMATCH); - } - - final CharSequence encrypted_encrypt_secret = sebClientConfig.hasEncryptionSecret() - ? this.clientCredentialService.encrypt(sebClientConfig.encryptSecret) - : null; - return (encrypted_encrypt_secret != null) ? encrypted_encrypt_secret.toString() : null; - } - - // check if same name already exists for the same institution - // if true an APIMessageException with a field validation error is thrown - private void checkUniqueName(final SebClientConfig sebClientConfig) { - - final Long otherWithSameName = this.sebClientConfigRecordMapper - .countByExample() - .where(SebClientConfigRecordDynamicSqlSupport.name, isEqualTo(sebClientConfig.name)) - .and(SebClientConfigRecordDynamicSqlSupport.institutionId, isEqualTo(sebClientConfig.institutionId)) - .and(SebClientConfigRecordDynamicSqlSupport.id, isNotEqualToWhenPresent(sebClientConfig.id)) - .build() - .execute(); - - if (otherWithSameName != null && otherWithSameName.longValue() > 0) { - throw new APIMessageException(APIMessage.fieldValidationError( - Domain.SEB_CLIENT_CONFIGURATION.ATTR_NAME, - "clientconfig:name:name.notunique")); - } - } - -} +/* + * 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.dao.impl; + +import static org.mybatis.dynamic.sql.SqlBuilder.*; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.StringUtils; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +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.ErrorMessage; +import ch.ethz.seb.sebserver.gbl.api.EntityType; +import ch.ethz.seb.sebserver.gbl.model.Domain; +import ch.ethz.seb.sebserver.gbl.model.EntityKey; +import ch.ethz.seb.sebserver.gbl.model.sebconfig.SebClientConfig; +import ch.ethz.seb.sebserver.gbl.model.sebconfig.SebClientConfig.ConfigPurpose; +import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; +import ch.ethz.seb.sebserver.gbl.util.Result; +import ch.ethz.seb.sebserver.gbl.util.Utils; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.SebClientConfigRecordDynamicSqlSupport; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.SebClientConfigRecordMapper; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.AdditionalAttributeRecord; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.SebClientConfigRecord; +import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.impl.BulkAction; +import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentialService; +import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentials; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.DAOLoggingSupport; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ResourceNotFoundException; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.SebClientConfigDAO; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.TransactionHandler; + +@Lazy +@Component +@WebServiceProfile +public class SebClientConfigDAOImpl implements SebClientConfigDAO { + + private final SebClientConfigRecordMapper sebClientConfigRecordMapper; + private final ClientCredentialService clientCredentialService; + private final AdditionalAttributesDAOImpl additionalAttributesDAO; + + protected SebClientConfigDAOImpl( + final SebClientConfigRecordMapper sebClientConfigRecordMapper, + final ClientCredentialService clientCredentialService, + final AdditionalAttributesDAOImpl additionalAttributesDAO) { + + this.sebClientConfigRecordMapper = sebClientConfigRecordMapper; + this.clientCredentialService = clientCredentialService; + this.additionalAttributesDAO = additionalAttributesDAO; + } + + @Override + public EntityType entityType() { + return EntityType.SEB_CLIENT_CONFIGURATION; + } + + @Override + @Transactional(readOnly = true) + public Result byPK(final Long id) { + return recordById(id) + .flatMap(this::toDomainModel); + } + + @Override + @Transactional(readOnly = true) + public Result> all(final Long institutionId, final Boolean active) { + return Result.tryCatch(() -> { + + final List records = (active != null) + ? this.sebClientConfigRecordMapper.selectByExample() + .where( + SebClientConfigRecordDynamicSqlSupport.institutionId, + isEqualToWhenPresent(institutionId)) + .and( + SebClientConfigRecordDynamicSqlSupport.active, + isEqualToWhenPresent(BooleanUtils.toIntegerObject(active))) + .build() + .execute() + : this.sebClientConfigRecordMapper.selectByExample().build().execute(); + + return records.stream() + .map(this::toDomainModel) + .flatMap(DAOLoggingSupport::logAndSkipOnError) + .collect(Collectors.toList()); + }); + } + + @Override + @Transactional(readOnly = true) + public Result> allMatching( + final FilterMap filterMap, + final Predicate predicate) { + + return Result.tryCatch(() -> this.sebClientConfigRecordMapper + .selectByExample() + .where( + SebClientConfigRecordDynamicSqlSupport.institutionId, + isEqualToWhenPresent(filterMap.getInstitutionId())) + .and( + SebClientConfigRecordDynamicSqlSupport.name, + isLikeWhenPresent(filterMap.getName())) + .and( + SebClientConfigRecordDynamicSqlSupport.date, + isGreaterThanOrEqualToWhenPresent(filterMap.getSebClientConfigFromTime())) + .and( + SebClientConfigRecordDynamicSqlSupport.active, + isEqualToWhenPresent(filterMap.getActiveAsInt())) + .build() + .execute() + .stream() + .map(this::toDomainModel) + .flatMap(DAOLoggingSupport::logAndSkipOnError) + .filter(predicate) + .collect(Collectors.toList())); + } + + @Override + public Result byClientName(final String clientName) { + return Result.tryCatch(() -> this.sebClientConfigRecordMapper + .selectByExample() + .where( + SebClientConfigRecordDynamicSqlSupport.clientName, + isEqualTo(clientName)) + .build() + .execute() + .stream() + .map(this::toDomainModel) + .flatMap(DAOLoggingSupport::logAndSkipOnError) + .collect(Utils.toSingleton())); + } + + @Override + @Transactional(readOnly = true) + public Result getConfigPasswordCipherByClientName(final String clientName) { + return Result.tryCatch(() -> { + + final SebClientConfigRecord record = this.sebClientConfigRecordMapper + .selectByExample() + .where( + SebClientConfigRecordDynamicSqlSupport.clientName, + isEqualTo(clientName)) + .and( + SebClientConfigRecordDynamicSqlSupport.active, + isNotEqualTo(0)) + .build() + .execute() + .stream() + .collect(Utils.toSingleton()); + + return record.getClientSecret(); + }); + } + + @Override + @Transactional(readOnly = true) + public boolean isActive(final String modelId) { + if (StringUtils.isBlank(modelId)) { + return false; + } + + return this.sebClientConfigRecordMapper.countByExample() + .where(SebClientConfigRecordDynamicSqlSupport.id, isEqualTo(Long.valueOf(modelId))) + .and(SebClientConfigRecordDynamicSqlSupport.active, isEqualTo(BooleanUtils.toInteger(true))) + .build() + .execute() > 0; + } + + @Override + @Transactional + public Result> setActive(final Set all, final boolean active) { + return Result.tryCatch(() -> { + + final List ids = extractListOfPKs(all); + final SebClientConfigRecord record = new SebClientConfigRecord( + null, null, null, null, null, null, null, + BooleanUtils.toIntegerObject(active)); + + this.sebClientConfigRecordMapper.updateByExampleSelective(record) + .where(SebClientConfigRecordDynamicSqlSupport.id, isIn(ids)) + .build() + .execute(); + + return ids.stream() + .map(id -> new EntityKey(id, EntityType.SEB_CLIENT_CONFIGURATION)) + .collect(Collectors.toList()); + }); + } + + @Override + @Transactional + public Result createNew(final SebClientConfig sebClientConfig) { + return this.clientCredentialService + .generatedClientCredentials() + .map(cc -> { + + checkUniqueName(sebClientConfig); + + final SebClientConfigRecord newRecord = new SebClientConfigRecord( + null, + sebClientConfig.institutionId, + sebClientConfig.name, + DateTime.now(DateTimeZone.UTC), + cc.clientIdAsString(), + cc.secretAsString(), + getEncryptionPassword(sebClientConfig), + BooleanUtils.toInteger(BooleanUtils.isTrue(sebClientConfig.active))); + + this.sebClientConfigRecordMapper + .insert(newRecord); + + saveAdditionalAttributes(sebClientConfig, newRecord.getId()); + + return newRecord; + }) + .flatMap(this::toDomainModel) + .onError(TransactionHandler::rollback); + } + + @Override + @Transactional + public Result save(final SebClientConfig sebClientConfig) { + return Result.tryCatch(() -> { + + checkUniqueName(sebClientConfig); + + final SebClientConfigRecord newRecord = new SebClientConfigRecord( + sebClientConfig.id, + null, + sebClientConfig.name, + null, + null, + null, + getEncryptionPassword(sebClientConfig), + null); + + this.sebClientConfigRecordMapper + .updateByPrimaryKeySelective(newRecord); + + saveAdditionalAttributes(sebClientConfig, newRecord.getId()); + + return this.sebClientConfigRecordMapper + .selectByPrimaryKey(sebClientConfig.id); + }) + .flatMap(this::toDomainModel) + .onError(TransactionHandler::rollback); + } + + @Override + @Transactional + public Result> delete(final Set all) { + return Result.tryCatch(() -> { + + final List ids = extractListOfPKs(all); + + this.sebClientConfigRecordMapper.deleteByExample() + .where(SebClientConfigRecordDynamicSqlSupport.id, isIn(ids)) + .build() + .execute(); + + return ids.stream() + .map(id -> new EntityKey(id, EntityType.SEB_CLIENT_CONFIGURATION)) + .collect(Collectors.toList()); + }); + } + + @Override + @Transactional(readOnly = true) + public Result> allOf(final Set pks) { + return Result.tryCatch(() -> this.sebClientConfigRecordMapper.selectByExample() + .where(SebClientConfigRecordDynamicSqlSupport.id, isIn(new ArrayList<>(pks))) + .build() + .execute() + .stream() + .map(this::toDomainModel) + .flatMap(DAOLoggingSupport::logAndSkipOnError) + .collect(Collectors.toList())); + } + + @Override + @Transactional(readOnly = true) + public Set getDependencies(final BulkAction bulkAction) { + // all of institution + if (bulkAction.sourceType == EntityType.INSTITUTION) { + return getDependencies(bulkAction, this::allIdsOfInstitution); + } + + return Collections.emptySet(); + } + + @Override + @Transactional(readOnly = true) + public Result getSebClientCredentials(final String modelId) { + return recordByModelId(modelId) + .map(rec -> new ClientCredentials( + rec.getClientName(), + rec.getClientSecret(), + null)); + } + + @Override + @Transactional(readOnly = true) + public Result getConfigPasswordCipher(final String modelId) { + return recordByModelId(modelId) + .map(SebClientConfigRecord::getEncryptSecret); + } + + private Result> allIdsOfInstitution(final EntityKey institutionKey) { + return Result.tryCatch(() -> this.sebClientConfigRecordMapper.selectIdsByExample() + .where(SebClientConfigRecordDynamicSqlSupport.institutionId, + isEqualTo(Long.valueOf(institutionKey.modelId))) + .build() + .execute() + .stream() + .map(id -> new EntityKey(id, EntityType.SEB_CLIENT_CONFIGURATION)) + .collect(Collectors.toList())); + } + + private Result recordByModelId(final String modelId) { + return Result.tryCatch(() -> recordById(Long.parseLong(modelId)).getOrThrow()); + } + + private Result recordById(final Long id) { + return Result.tryCatch(() -> { + final SebClientConfigRecord record = this.sebClientConfigRecordMapper + .selectByPrimaryKey(id); + + if (record == null) { + throw new ResourceNotFoundException( + EntityType.SEB_CLIENT_CONFIGURATION, + String.valueOf(id)); + } + return record; + }); + } + + private Result toDomainModel(final SebClientConfigRecord record) { + + Map additionalAttributes = this.additionalAttributesDAO + .getAdditionalAttributes( + EntityType.SEB_CLIENT_CONFIGURATION, + record.getId()) + .getOrThrow() + .stream() + .collect(Collectors.toMap( + AdditionalAttributeRecord::getName, + Function.identity())); + + additionalAttributes.get(SebClientConfig.ATTR_CONFIG_PURPOSE); + + return Result.tryCatch(() -> new SebClientConfig( + record.getId(), + record.getInstitutionId(), + record.getName(), + additionalAttributes.containsKey(SebClientConfig.ATTR_CONFIG_PURPOSE) + ? ConfigPurpose.valueOf(additionalAttributes.get(SebClientConfig.ATTR_CONFIG_PURPOSE).getValue()) + : ConfigPurpose.START_EXAM, + additionalAttributes.containsKey(SebClientConfig.ATTR_FALLBACK) && + BooleanUtils.toBoolean(additionalAttributes.get(SebClientConfig.ATTR_FALLBACK).getValue()), + additionalAttributes.containsKey(SebClientConfig.ATTR_FALLBACK_START_URL) + ? additionalAttributes.get(SebClientConfig.ATTR_FALLBACK_START_URL).getValue() + : null, + additionalAttributes.containsKey(SebClientConfig.ATTR_FALLBACK_TIMEOUT) + ? Long.parseLong(additionalAttributes.get(SebClientConfig.ATTR_FALLBACK_TIMEOUT).getValue()) + : null, + additionalAttributes.containsKey(SebClientConfig.ATTR_FALLBACK_ATTEMPTS) + ? Short.parseShort(additionalAttributes.get(SebClientConfig.ATTR_FALLBACK_ATTEMPTS).getValue()) + : null, + additionalAttributes.containsKey(SebClientConfig.ATTR_FALLBACK_ATTEMPT_INTERVAL) + ? Short.parseShort(additionalAttributes.get(SebClientConfig.ATTR_FALLBACK_ATTEMPT_INTERVAL).getValue()) + : null, + additionalAttributes.containsKey(SebClientConfig.ATTR_FALLBACK_PASSWORD) + ? additionalAttributes.get(SebClientConfig.ATTR_FALLBACK_PASSWORD).getValue() + : null, + null, + additionalAttributes.containsKey(SebClientConfig.ATTR_QUIT_PASSWORD) + ? additionalAttributes.get(SebClientConfig.ATTR_QUIT_PASSWORD).getValue() + : null, + null, + record.getDate(), + record.getEncryptSecret(), + null, + BooleanUtils.toBooleanObject(record.getActive()))); + } + + private String getEncryptionPassword(final SebClientConfig sebClientConfig) { + if (sebClientConfig.hasEncryptionSecret() && + !sebClientConfig.encryptSecret.equals(sebClientConfig.encryptSecretConfirm)) { + throw new APIMessageException(ErrorMessage.PASSWORD_MISMATCH); + } + + final CharSequence encrypted_encrypt_secret = sebClientConfig.hasEncryptionSecret() + ? this.clientCredentialService.encrypt(sebClientConfig.encryptSecret) + : null; + return (encrypted_encrypt_secret != null) ? encrypted_encrypt_secret.toString() : null; + } + + // check if same name already exists for the same institution + // if true an APIMessageException with a field validation error is thrown + private void checkUniqueName(final SebClientConfig sebClientConfig) { + + final Long otherWithSameName = this.sebClientConfigRecordMapper + .countByExample() + .where(SebClientConfigRecordDynamicSqlSupport.name, isEqualTo(sebClientConfig.name)) + .and(SebClientConfigRecordDynamicSqlSupport.institutionId, isEqualTo(sebClientConfig.institutionId)) + .and(SebClientConfigRecordDynamicSqlSupport.id, isNotEqualToWhenPresent(sebClientConfig.id)) + .build() + .execute(); + + if (otherWithSameName != null && otherWithSameName > 0) { + throw new APIMessageException(APIMessage.fieldValidationError( + Domain.SEB_CLIENT_CONFIGURATION.ATTR_NAME, + "clientconfig:name:name.notunique")); + } + } + + private void saveAdditionalAttributes(SebClientConfig sebClientConfig, Long configId) { + this.additionalAttributesDAO.saveAdditionalAttribute( + EntityType.SEB_CLIENT_CONFIGURATION, + configId, + SebClientConfig.ATTR_CONFIG_PURPOSE, + (sebClientConfig.configPurpose != null) + ? sebClientConfig.configPurpose.name() + : ConfigPurpose.START_EXAM.name()); + + this.additionalAttributesDAO.saveAdditionalAttribute( + EntityType.SEB_CLIENT_CONFIGURATION, + configId, + SebClientConfig.ATTR_FALLBACK, + String.valueOf(BooleanUtils.isTrue(sebClientConfig.fallback))); + + if (BooleanUtils.isTrue(sebClientConfig.fallback)) { + this.additionalAttributesDAO.saveAdditionalAttribute( + EntityType.SEB_CLIENT_CONFIGURATION, + configId, + SebClientConfig.ATTR_FALLBACK_START_URL, + sebClientConfig.fallbackStartURL); + } else { + this.additionalAttributesDAO.delete( + configId, + SebClientConfig.ATTR_FALLBACK_START_URL); + } + + if (BooleanUtils.isTrue(sebClientConfig.fallback)) { + this.additionalAttributesDAO.saveAdditionalAttribute( + EntityType.SEB_CLIENT_CONFIGURATION, + configId, + SebClientConfig.ATTR_FALLBACK_TIMEOUT, + sebClientConfig.fallbackTimeout.toString()); + } else { + this.additionalAttributesDAO.delete( + configId, + SebClientConfig.ATTR_FALLBACK_TIMEOUT); + } + + if (BooleanUtils.isTrue(sebClientConfig.fallback)) { + this.additionalAttributesDAO.saveAdditionalAttribute( + EntityType.SEB_CLIENT_CONFIGURATION, + configId, + SebClientConfig.ATTR_FALLBACK_ATTEMPTS, + sebClientConfig.fallbackAttempts.toString()); + } else { + this.additionalAttributesDAO.delete( + configId, + SebClientConfig.ATTR_FALLBACK_ATTEMPTS); + } + + if (BooleanUtils.isTrue(sebClientConfig.fallback)) { + this.additionalAttributesDAO.saveAdditionalAttribute( + EntityType.SEB_CLIENT_CONFIGURATION, + configId, + SebClientConfig.ATTR_FALLBACK_ATTEMPT_INTERVAL, + sebClientConfig.fallbackAttemptInterval.toString()); + } else { + this.additionalAttributesDAO.delete( + configId, + SebClientConfig.ATTR_FALLBACK_ATTEMPT_INTERVAL); + } + + if (BooleanUtils.isTrue(sebClientConfig.fallback) && StringUtils.isNotBlank(sebClientConfig.fallbackPassword)) { + this.additionalAttributesDAO.saveAdditionalAttribute( + EntityType.SEB_CLIENT_CONFIGURATION, + configId, + SebClientConfig.ATTR_FALLBACK_PASSWORD, + this.clientCredentialService.encrypt(sebClientConfig.fallbackPassword).toString()); + } else { + this.additionalAttributesDAO.delete( + configId, + SebClientConfig.ATTR_FALLBACK_PASSWORD); + } + + if (BooleanUtils.isTrue(sebClientConfig.fallback) && StringUtils.isNotBlank(sebClientConfig.quitPassword)) { + this.additionalAttributesDAO.saveAdditionalAttribute( + EntityType.SEB_CLIENT_CONFIGURATION, + configId, + SebClientConfig.ATTR_QUIT_PASSWORD, + this.clientCredentialService.encrypt(sebClientConfig.quitPassword).toString()); + } else { + this.additionalAttributesDAO.delete( + configId, + SebClientConfig.ATTR_QUIT_PASSWORD); + } + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/ClientConfigService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/ClientConfigService.java index 4696d2b0..1a3f1a61 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/ClientConfigService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/ClientConfigService.java @@ -1,93 +1,69 @@ -/* - * 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; - -import java.io.OutputStream; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.cache.annotation.CacheEvict; -import org.springframework.cache.annotation.Cacheable; -import org.springframework.context.event.EventListener; -import org.springframework.security.oauth2.provider.ClientDetails; - -import ch.ethz.seb.sebserver.gbl.model.sebconfig.SebClientConfig; -import ch.ethz.seb.sebserver.gbl.util.Result; -import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.impl.BulkActionEvent; - -public interface ClientConfigService { - - Logger log = LoggerFactory.getLogger(ClientConfigService.class); - - public static final String EXAM_CLIENT_DETAILS_CACHE = "EXAM_CLIENT_DETAILS_CACHE"; - - static String SEB_CLIENT_CONFIG_EXAMPLE_XML = - " \r\n" + - " sebMode\r\n" + - " 1\r\n" + - " sebConfigPurpose\r\n" + - " 1\r\n" + - " sebServerFallback\r\n" + - " <%s />\r\n" + - " %s" + - " sebServerURL\r\n" + - " %s\r\n" + - " sebServerConfiguration\r\n" + - " \r\n" + - " institution\r\n" + - " %s\r\n" + - " clientName\r\n" + - " %s\r\n" + - " clientSecret\r\n" + - " %s\r\n" + - " apiDiscovery\r\n" + - " %s\r\n" + - " \r\n" + - " \r\n"; - - /** Indicates if there is any SebClientConfiguration for a specified institution. - * - * @param institutionId the institution identifier - * @return true if there is any SebClientConfiguration for a specified institution. False otherwise */ - boolean hasSebClientConfigurationForInstitution(Long institutionId); - - /** Use this to auto-generate a SebClientConfiguration for a specified institution. - * clientName and clientSecret are randomly generated. - * - * @param institutionId the institution identifier - * @return the created SebClientConfig */ - Result autoCreateSebClientConfigurationForInstitution(Long institutionId); - - /** Use this to export a specified SebClientConfiguration within a given OutputStream. - * The SEB Client Configuration is exported in the defined SEB Configuration format - * as described here: https://www.safeexambrowser.org/developer/seb-file-format.html - * - * @param out OutputStream to write the export to - * @param modelId the model identifier of the SebClientConfiguration to export */ - void exportSebClientConfiguration( - OutputStream out, - final String modelId); - - /** Get the ClientDetails for given client name that identifies a SebClientConfig entry. - * - * @param clientName the client name of a SebClientConfig entry - * @return Result refer to the ClientDetails for the specified clientName or to an error if happened */ - @Cacheable( - cacheNames = EXAM_CLIENT_DETAILS_CACHE, - key = "#clientName", - unless = "#result.hasError()") - Result getClientConfigDetails(String clientName); - - @CacheEvict( - cacheNames = EXAM_CLIENT_DETAILS_CACHE, - allEntries = true) - @EventListener(BulkActionEvent.class) - void flushClientConfigData(BulkActionEvent event); - -} +/* + * 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; + +import java.io.OutputStream; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.context.event.EventListener; +import org.springframework.security.oauth2.provider.ClientDetails; + +import ch.ethz.seb.sebserver.gbl.model.sebconfig.SebClientConfig; +import ch.ethz.seb.sebserver.gbl.util.Result; +import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.impl.BulkActionEvent; + +public interface ClientConfigService { + + Logger log = LoggerFactory.getLogger(ClientConfigService.class); + + String EXAM_CLIENT_DETAILS_CACHE = "EXAM_CLIENT_DETAILS_CACHE"; + + /** Indicates if there is any SebClientConfiguration for a specified institution. + * + * @param institutionId the institution identifier + * @return true if there is any SebClientConfiguration for a specified institution. False otherwise */ + boolean hasSebClientConfigurationForInstitution(Long institutionId); + + /** Use this to auto-generate a SebClientConfiguration for a specified institution. + * clientName and clientSecret are randomly generated. + * + * @param institutionId the institution identifier + * @return the created SebClientConfig */ + Result autoCreateSebClientConfigurationForInstitution(Long institutionId); + + /** Use this to export a specified SebClientConfiguration within a given OutputStream. + * The SEB Client Configuration is exported in the defined SEB Configuration format + * as described here: https://www.safeexambrowser.org/developer/seb-file-format.html + * + * @param out OutputStream to write the export to + * @param modelId the model identifier of the SebClientConfiguration to export */ + void exportSebClientConfiguration( + OutputStream out, + final String modelId); + + /** Get the ClientDetails for given client name that identifies a SebClientConfig entry. + * + * @param clientName the client name of a SebClientConfig entry + * @return Result refer to the ClientDetails for the specified clientName or to an error if happened */ + @Cacheable( + cacheNames = EXAM_CLIENT_DETAILS_CACHE, + key = "#clientName", + unless = "#result.hasError()") + Result getClientConfigDetails(String clientName); + + @CacheEvict( + cacheNames = EXAM_CLIENT_DETAILS_CACHE, + allEntries = true) + @EventListener(BulkActionEvent.class) + void flushClientConfigData(BulkActionEvent event); + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/ClientConfigServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/ClientConfigServiceImpl.java index 3dc498f1..7be036c9 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/ClientConfigServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/ClientConfigServiceImpl.java @@ -1,314 +1,377 @@ -/* - * 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.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.PipedInputStream; -import java.io.PipedOutputStream; -import java.nio.charset.StandardCharsets; -import java.util.Collection; -import java.util.Collections; -import java.util.UUID; - -import org.apache.commons.io.IOUtils; -import org.apache.commons.lang3.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.context.annotation.Lazy; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.common.OAuth2AccessToken; -import org.springframework.security.oauth2.provider.ClientDetails; -import org.springframework.security.oauth2.provider.client.BaseClientDetails; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.stereotype.Service; - -import ch.ethz.seb.sebserver.WebSecurityConfig; -import ch.ethz.seb.sebserver.gbl.Constants; -import ch.ethz.seb.sebserver.gbl.api.API.BulkActionType; -import ch.ethz.seb.sebserver.gbl.api.EntityType; -import ch.ethz.seb.sebserver.gbl.model.EntityKey; -import ch.ethz.seb.sebserver.gbl.model.institution.Institution; -import ch.ethz.seb.sebserver.gbl.model.sebconfig.SebClientConfig; -import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; -import ch.ethz.seb.sebserver.gbl.util.Result; -import ch.ethz.seb.sebserver.gbl.util.Utils; -import ch.ethz.seb.sebserver.webservice.WebserviceInfo; -import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.impl.BulkAction; -import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.impl.BulkActionEvent; -import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentialService; -import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentials; -import ch.ethz.seb.sebserver.webservice.servicelayer.dao.InstitutionDAO; -import ch.ethz.seb.sebserver.webservice.servicelayer.dao.SebClientConfigDAO; -import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ClientConfigService; -import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.SebConfigEncryptionService; -import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.SebConfigEncryptionService.Strategy; -import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ZipService; -import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.impl.SebConfigEncryptionServiceImpl.EncryptionContext; -import ch.ethz.seb.sebserver.webservice.weblayer.oauth.WebserviceResourceConfiguration; - -@Lazy -@Service -@WebServiceProfile -public class ClientConfigServiceImpl implements ClientConfigService { - - private static final Logger log = LoggerFactory.getLogger(ClientConfigServiceImpl.class); - - private final InstitutionDAO institutionDAO; - private final SebClientConfigDAO sebClientConfigDAO; - private final ClientCredentialService clientCredentialService; - private final SebConfigEncryptionService sebConfigEncryptionService; - private final PasswordEncoder clientPasswordEncoder; - private final ZipService zipService; - private final TokenStore tokenStore; - private final WebserviceInfo webserviceInfo; - - protected ClientConfigServiceImpl( - final InstitutionDAO institutionDAO, - final SebClientConfigDAO sebClientConfigDAO, - final ClientCredentialService clientCredentialService, - final SebConfigEncryptionService sebConfigEncryptionService, - final ZipService zipService, - final TokenStore tokenStore, - @Qualifier(WebSecurityConfig.CLIENT_PASSWORD_ENCODER_BEAN_NAME) final PasswordEncoder clientPasswordEncoder, - final WebserviceInfo webserviceInfo) { - - this.institutionDAO = institutionDAO; - this.sebClientConfigDAO = sebClientConfigDAO; - this.clientCredentialService = clientCredentialService; - this.sebConfigEncryptionService = sebConfigEncryptionService; - this.zipService = zipService; - this.clientPasswordEncoder = clientPasswordEncoder; - this.tokenStore = tokenStore; - this.webserviceInfo = webserviceInfo; - } - - @Override - public boolean hasSebClientConfigurationForInstitution(final Long institutionId) { - final Result> all = this.sebClientConfigDAO.all(institutionId, true); - return all != null && !all.hasError() && !all.getOrThrow().isEmpty(); - } - - @Override - public Result autoCreateSebClientConfigurationForInstitution(final Long institutionId) { - return Result.tryCatch(() -> { - final Institution institution = this.institutionDAO - .byPK(institutionId) - .getOrThrow(); - - return new SebClientConfig( - null, - institutionId, - institution.name + "_" + UUID.randomUUID(), - null, - null, - null, - null, - true); - }) - .flatMap(this.sebClientConfigDAO::createNew); - } - - @Override - public Result getClientConfigDetails(final String clientName) { - return this.getEncodedClientConfigSecret(clientName) - .map(pwd -> { - - final BaseClientDetails baseClientDetails = new BaseClientDetails( - Utils.toString(clientName), - WebserviceResourceConfiguration.EXAM_API_RESOURCE_ID, - null, - Constants.OAUTH2_GRANT_TYPE_CLIENT_CREDENTIALS, - StringUtils.EMPTY); - - baseClientDetails.setScope(Collections.emptySet()); - baseClientDetails.setClientSecret(Utils.toString(pwd)); - baseClientDetails.setAccessTokenValiditySeconds(-1); // not expiring - - if (log.isDebugEnabled()) { - log.debug("Created new BaseClientDetails for id: {}", clientName); - } - - return baseClientDetails; - }); - } - - @Override - public void exportSebClientConfiguration( - final OutputStream output, - final String modelId) { - - final SebClientConfig config = this.sebClientConfigDAO - .byModelId(modelId).getOrThrow(); - - final CharSequence encryptionPassword = this.sebClientConfigDAO - .getConfigPasswortCipher(config.getModelId()) - .getOr(null); - - final String plainTextConfig = getPlainXMLConfig(config); - - PipedOutputStream pOut = null; - PipedInputStream pIn = null; - - try { - - // zip the plain text - final InputStream plainIn = IOUtils.toInputStream( - Constants.XML_VERSION_HEADER + - Constants.XML_DOCTYPE_HEADER + - Constants.XML_PLIST_START_V1 + - plainTextConfig + - Constants.XML_PLIST_END, - StandardCharsets.UTF_8.name()); - - pOut = new PipedOutputStream(); - pIn = new PipedInputStream(pOut); - - this.zipService.write(pOut, plainIn); - - if (encryptionPassword != null) { - passwordEncryption(output, encryptionPassword, pIn); - } else { - this.sebConfigEncryptionService.streamEncrypted( - output, - pIn, - EncryptionContext.contextOfPlainText()); - } - - if (log.isDebugEnabled()) { - log.debug("*** Finished Seb client configuration download streaming composition"); - } - - } catch (final Exception e) { - log.error("Error while zip and encrypt seb client config stream: ", e); - try { - if (pIn != null) { - pIn.close(); - } - } catch (final IOException e1) { - log.error("Failed to close PipedInputStream: ", e1); - } - try { - if (pOut != null) { - pOut.close(); - } - } catch (final IOException e1) { - log.error("Failed to close PipedOutputStream: ", e1); - } - } - } - - public String getPlainXMLConfig(final SebClientConfig config) { - - final ClientCredentials sebClientCredentials = this.sebClientConfigDAO - .getSebClientCredentials(config.getModelId()) - .getOrThrow(); - - final CharSequence plainClientId = sebClientCredentials.clientId; - final CharSequence plainClientSecret = this.clientCredentialService - .getPlainClientSecret(sebClientCredentials); - - final String plainTextConfig = extractXML( - config, - plainClientId, - plainClientSecret); - - return plainTextConfig; - } - - private String extractXML( - final SebClientConfig config, - final CharSequence plainClientId, - final CharSequence plainClientSecret) { - - final String plainTextConfig = String.format( - SEB_CLIENT_CONFIG_EXAMPLE_XML, - (StringUtils.isNotBlank(config.fallbackStartURL)) - ? "true" - : "false", - (StringUtils.isNotBlank(config.fallbackStartURL)) - ? "startURL\r\n " + config.fallbackStartURL + "\r\n" - : "", - this.webserviceInfo.getExternalServerURL(), - String.valueOf(config.institutionId), - plainClientId, - plainClientSecret, - this.webserviceInfo.getDiscoveryEndpoint()); - - if (log.isDebugEnabled()) { - log.debug("SEB client configuration export:\n {}", plainTextConfig); - } - - return plainTextConfig; - } - - @Override - public void flushClientConfigData(final BulkActionEvent event) { - try { - final BulkAction bulkAction = event.getBulkAction(); - - if (bulkAction.type == BulkActionType.DEACTIVATE || - bulkAction.type == BulkActionType.HARD_DELETE) { - - bulkAction.extractKeys(EntityType.SEB_CLIENT_CONFIGURATION) - .stream() - .forEach(this::flushClientConfigData); - } - - } catch (final Exception e) { - log.error("Unexpected error while trying to flush ClientConfig data ", e); - } - } - - private void flushClientConfigData(final EntityKey key) { - try { - final String clientName = this.sebClientConfigDAO.getSebClientCredentials(key.modelId) - .getOrThrow() - .clientIdAsString(); - - final Collection tokensByClientId = this.tokenStore.findTokensByClientId(clientName); - tokensByClientId.stream() - .forEach(token -> this.tokenStore.removeAccessToken(token)); - } catch (final Exception e) { - log.error("Unexpected error while trying to flush ClientConfig data for {}", key, e); - } - } - - private void passwordEncryption( - final OutputStream output, - final CharSequence encryptionPassword, - final InputStream input) { - - if (log.isDebugEnabled()) { - log.debug("*** Seb client configuration with password based encryption"); - } - - final CharSequence encryptionPasswordPlaintext = this.clientCredentialService - .decrypt(encryptionPassword); - - this.sebConfigEncryptionService.streamEncrypted( - output, - input, - EncryptionContext.contextOf( - Strategy.PASSWORD_PSWD, - encryptionPasswordPlaintext)); - } - - /** Get a encoded clientSecret for the SebClientConfiguration with specified clientId/clientName. - * - * @param clientId the clientId/clientName - * @return encoded clientSecret for that SebClientConfiguration with clientId or null of not existing */ - private Result getEncodedClientConfigSecret(final String clientId) { - return this.sebClientConfigDAO.getConfigPasswortCipherByClientName(clientId) - .map(cipher -> this.clientPasswordEncoder.encode(this.clientCredentialService.decrypt(cipher))); - } - -} +/* + * 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 ch.ethz.seb.sebserver.WebSecurityConfig; +import ch.ethz.seb.sebserver.gbl.Constants; +import ch.ethz.seb.sebserver.gbl.api.API.BulkActionType; +import ch.ethz.seb.sebserver.gbl.api.EntityType; +import ch.ethz.seb.sebserver.gbl.model.EntityKey; +import ch.ethz.seb.sebserver.gbl.model.institution.Institution; +import ch.ethz.seb.sebserver.gbl.model.sebconfig.SebClientConfig; +import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; +import ch.ethz.seb.sebserver.gbl.util.Result; +import ch.ethz.seb.sebserver.gbl.util.Utils; +import ch.ethz.seb.sebserver.webservice.WebserviceInfo; +import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.impl.BulkAction; +import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.impl.BulkActionEvent; +import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentialService; +import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentials; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.InstitutionDAO; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.SebClientConfigDAO; +import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ClientConfigService; +import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.SebConfigEncryptionService; +import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.SebConfigEncryptionService.Strategy; +import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ZipService; +import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.impl.SebConfigEncryptionServiceImpl.EncryptionContext; +import ch.ethz.seb.sebserver.webservice.weblayer.oauth.WebserviceResourceConfiguration; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Lazy; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.oauth2.common.OAuth2AccessToken; +import org.springframework.security.oauth2.provider.ClientDetails; +import org.springframework.security.oauth2.provider.client.BaseClientDetails; +import org.springframework.security.oauth2.provider.token.TokenStore; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.Collections; +import java.util.UUID; + +@Lazy +@Service +@WebServiceProfile +public class ClientConfigServiceImpl implements ClientConfigService { + + private static final Logger log = LoggerFactory.getLogger(ClientConfigServiceImpl.class); + + private static final String SEB_CLIENT_CONFIG_TEMPLATE_XML = + " \r\n" + + " sebMode\r\n" + + " 1\r\n" + + " sebConfigPurpose\r\n" + + " %s\r\n" + + " sebServerFallback\r\n" + + " <%s />\r\n" + + "%s" + + " sebServerURL\r\n" + + " %s\r\n" + + " sebServerConfiguration\r\n" + + " \r\n" + + " institution\r\n" + + " %s\r\n" + + " clientName\r\n" + + " %s\r\n" + + " clientSecret\r\n" + + " %s\r\n" + + " apiDiscovery\r\n" + + " %s\r\n" + + " \r\n" + + " \r\n"; + + private final static String SEB_CLIENT_CONFIG_INTEGER_TEMPLATE = + " %s\r\n" + + " %s\r\n"; + + private final static String SEB_CLIENT_CONFIG_STRING_TEMPLATE = + " %s\r\n" + + " %s\r\n"; + + private final InstitutionDAO institutionDAO; + private final SebClientConfigDAO sebClientConfigDAO; + private final ClientCredentialService clientCredentialService; + private final SebConfigEncryptionService sebConfigEncryptionService; + private final PasswordEncoder clientPasswordEncoder; + private final ZipService zipService; + private final TokenStore tokenStore; + private final WebserviceInfo webserviceInfo; + + protected ClientConfigServiceImpl( + final InstitutionDAO institutionDAO, + final SebClientConfigDAO sebClientConfigDAO, + final ClientCredentialService clientCredentialService, + final SebConfigEncryptionService sebConfigEncryptionService, + final ZipService zipService, + final TokenStore tokenStore, + @Qualifier(WebSecurityConfig.CLIENT_PASSWORD_ENCODER_BEAN_NAME) final PasswordEncoder clientPasswordEncoder, + final WebserviceInfo webserviceInfo) { + + this.institutionDAO = institutionDAO; + this.sebClientConfigDAO = sebClientConfigDAO; + this.clientCredentialService = clientCredentialService; + this.sebConfigEncryptionService = sebConfigEncryptionService; + this.zipService = zipService; + this.clientPasswordEncoder = clientPasswordEncoder; + this.tokenStore = tokenStore; + this.webserviceInfo = webserviceInfo; + } + + @Override + public boolean hasSebClientConfigurationForInstitution(final Long institutionId) { + final Result> all = this.sebClientConfigDAO.all(institutionId, true); + return all != null && !all.hasError() && !all.getOrThrow().isEmpty(); + } + + @Override + public Result autoCreateSebClientConfigurationForInstitution(final Long institutionId) { + return Result.tryCatch(() -> { + final Institution institution = this.institutionDAO + .byPK(institutionId) + .getOrThrow(); + + return new SebClientConfig( + null, + institutionId, + institution.name + "_" + UUID.randomUUID(), + null, + false, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + true); + }) + .flatMap(this.sebClientConfigDAO::createNew); + } + + @Override + public Result getClientConfigDetails(final String clientName) { + return this.getEncodedClientConfigSecret(clientName) + .map(pwd -> { + + final BaseClientDetails baseClientDetails = new BaseClientDetails( + Utils.toString(clientName), + WebserviceResourceConfiguration.EXAM_API_RESOURCE_ID, + null, + Constants.OAUTH2_GRANT_TYPE_CLIENT_CREDENTIALS, + StringUtils.EMPTY); + + baseClientDetails.setScope(Collections.emptySet()); + baseClientDetails.setClientSecret(Utils.toString(pwd)); + baseClientDetails.setAccessTokenValiditySeconds(-1); // not expiring + + if (log.isDebugEnabled()) { + log.debug("Created new BaseClientDetails for id: {}", clientName); + } + + return baseClientDetails; + }); + } + + @Override + public void exportSebClientConfiguration( + final OutputStream output, + final String modelId) { + + final SebClientConfig config = this.sebClientConfigDAO + .byModelId(modelId).getOrThrow(); + + final CharSequence encryptionPassword = this.sebClientConfigDAO + .getConfigPasswordCipher(config.getModelId()) + .getOr(null); + + final String plainTextXMLContent = extractXMLContent(config); + + PipedOutputStream pOut = null; + PipedInputStream pIn = null; + + try { + + // zip the plain text + final InputStream plainIn = IOUtils.toInputStream( + Constants.XML_VERSION_HEADER + + Constants.XML_DOCTYPE_HEADER + + Constants.XML_PLIST_START_V1 + + plainTextXMLContent + + Constants.XML_PLIST_END, + StandardCharsets.UTF_8.name()); + + pOut = new PipedOutputStream(); + pIn = new PipedInputStream(pOut); + + this.zipService.write(pOut, plainIn); + + if (encryptionPassword != null) { + passwordEncryption(output, encryptionPassword, pIn); + } else { + this.sebConfigEncryptionService.streamEncrypted( + output, + pIn, + EncryptionContext.contextOfPlainText()); + } + + if (log.isDebugEnabled()) { + log.debug("*** Finished Seb client configuration download streaming composition"); + } + + } catch (final Exception e) { + log.error("Error while zip and encrypt seb client config stream: ", e); + try { + if (pIn != null) { + pIn.close(); + } + } catch (final IOException e1) { + log.error("Failed to close PipedInputStream: ", e1); + } + try { + if (pOut != null) { + pOut.close(); + } + } catch (final IOException e1) { + log.error("Failed to close PipedOutputStream: ", e1); + } + } + } + + private String extractXMLContent(final SebClientConfig config) { + + String fallbackAddition = ""; + if (BooleanUtils.isTrue(config.fallback)) { + fallbackAddition += String.format( + SEB_CLIENT_CONFIG_STRING_TEMPLATE, + SebClientConfig.ATTR_FALLBACK_START_URL, + config.fallbackStartURL); + + fallbackAddition += String.format( + SEB_CLIENT_CONFIG_INTEGER_TEMPLATE, + SebClientConfig.ATTR_FALLBACK_TIMEOUT, + config.fallbackTimeout); + + fallbackAddition += String.format( + SEB_CLIENT_CONFIG_INTEGER_TEMPLATE, + SebClientConfig.ATTR_FALLBACK_ATTEMPTS, + config.fallbackAttempts); + + fallbackAddition += String.format( + SEB_CLIENT_CONFIG_INTEGER_TEMPLATE, + SebClientConfig.ATTR_FALLBACK_ATTEMPT_INTERVAL, + config.fallbackAttemptInterval); + + if (StringUtils.isNotBlank(config.fallbackPassword)) { + CharSequence decrypt = clientCredentialService.decrypt(config.fallbackPassword); + fallbackAddition += String.format( + SEB_CLIENT_CONFIG_STRING_TEMPLATE, + SebClientConfig.ATTR_FALLBACK_PASSWORD, + Utils.hash_SHA_256_Base_16(decrypt)); + } + + if (StringUtils.isNotBlank(config.quitPassword)) { + CharSequence decrypt = clientCredentialService.decrypt(config.quitPassword); + fallbackAddition += String.format( + SEB_CLIENT_CONFIG_STRING_TEMPLATE, + SebClientConfig.ATTR_QUIT_PASSWORD, + Utils.hash_SHA_256_Base_16(decrypt)); + } + } + + final ClientCredentials sebClientCredentials = this.sebClientConfigDAO + .getSebClientCredentials(config.getModelId()) + .getOrThrow(); + final CharSequence plainClientId = sebClientCredentials.clientId; + final CharSequence plainClientSecret = this.clientCredentialService + .getPlainClientSecret(sebClientCredentials); + + final String plainTextConfig = String.format( + SEB_CLIENT_CONFIG_TEMPLATE_XML, + config.configPurpose.ordinal(), + (StringUtils.isNotBlank(config.fallbackStartURL)) + ? "true" + : "false", + fallbackAddition, + this.webserviceInfo.getExternalServerURL(), + config.institutionId, + plainClientId, + plainClientSecret, + this.webserviceInfo.getDiscoveryEndpoint()); + + if (log.isDebugEnabled()) { + log.debug("SEB client configuration export:\n {}", plainTextConfig); + } + + return plainTextConfig; + } + + @Override + public void flushClientConfigData(final BulkActionEvent event) { + try { + final BulkAction bulkAction = event.getBulkAction(); + + if (bulkAction.type == BulkActionType.DEACTIVATE || + bulkAction.type == BulkActionType.HARD_DELETE) { + + bulkAction.extractKeys(EntityType.SEB_CLIENT_CONFIGURATION) + .forEach(this::flushClientConfigData); + } + + } catch (final Exception e) { + log.error("Unexpected error while trying to flush ClientConfig data ", e); + } + } + + private void flushClientConfigData(final EntityKey key) { + try { + final String clientName = this.sebClientConfigDAO.getSebClientCredentials(key.modelId) + .getOrThrow() + .clientIdAsString(); + + final Collection tokensByClientId = this.tokenStore.findTokensByClientId(clientName); + tokensByClientId.forEach(this.tokenStore::removeAccessToken); + } catch (final Exception e) { + log.error("Unexpected error while trying to flush ClientConfig data for {}", key, e); + } + } + + private void passwordEncryption( + final OutputStream output, + final CharSequence encryptionPassword, + final InputStream input) { + + if (log.isDebugEnabled()) { + log.debug("*** Seb client configuration with password based encryption"); + } + + final CharSequence encryptionPasswordPlaintext = this.clientCredentialService + .decrypt(encryptionPassword); + + this.sebConfigEncryptionService.streamEncrypted( + output, + input, + EncryptionContext.contextOf( + Strategy.PASSWORD_PSWD, + encryptionPasswordPlaintext)); + } + + /** Get a encoded clientSecret for the SebClientConfiguration with specified clientId/clientName. + * + * @param clientId the clientId/clientName + * @return encoded clientSecret for that SebClientConfiguration with clientId or null of not existing */ + private Result getEncodedClientConfigSecret(final String clientId) { + return this.sebClientConfigDAO.getConfigPasswordCipherByClientName(clientId) + .map(cipher -> this.clientPasswordEncoder.encode(this.clientCredentialService.decrypt(cipher))); + } + +} 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 feaa5c0d..fd547a81 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 @@ -1,313 +1,318 @@ -/* - * 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.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.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.function.Function; -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; -import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationAttribute; -import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationValue; -import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; -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; -import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ZipService; - -@Lazy -@Component -@WebServiceProfile -public class ExamConfigIO { - - private static final Logger log = LoggerFactory.getLogger(ExamConfigIO.class); - - private static final byte[] XML_VERSION_HEADER_UTF_8 = Utils.toByteArray(Constants.XML_VERSION_HEADER); - private static final byte[] XML_DOCTYPE_HEADER_UTF_8 = Utils.toByteArray(Constants.XML_DOCTYPE_HEADER); - private static final byte[] XML_PLIST_START_V1_UTF_8 = Utils.toByteArray(Constants.XML_PLIST_START_V1); - private static final byte[] XML_PLIST_END_UTF_8 = Utils.toByteArray(Constants.XML_PLIST_END); - private static final byte[] XML_DICT_START_UTF_8 = Utils.toByteArray(Constants.XML_DICT_START); - private static final byte[] XML_DICT_END_UTF_8 = Utils.toByteArray(Constants.XML_DICT_END); - private static final byte[] JSON_START = Utils.toByteArray("{"); - private static final byte[] JSON_END = Utils.toByteArray("}"); - private static final byte[] JSON_SEPARATOR = Utils.toByteArray(Constants.LIST_SEPARATOR); - - private final ConfigurationAttributeDAO configurationAttributeDAO; - private final ConfigurationValueDAO configurationValueDAO; - private final ConfigurationDAO configurationDAO; - private final AttributeValueConverterService attributeValueConverterService; - private final ZipService zipService; - - protected ExamConfigIO( - final ConfigurationAttributeDAO configurationAttributeDAO, - final ConfigurationValueDAO configurationValueDAO, - final ConfigurationDAO configurationDAO, - final AttributeValueConverterService attributeValueConverterService, - final ZipService zipService) { - - this.configurationAttributeDAO = configurationAttributeDAO; - this.configurationValueDAO = configurationValueDAO; - this.configurationDAO = configurationDAO; - this.attributeValueConverterService = attributeValueConverterService; - this.zipService = zipService; - } - - @Async(AsyncServiceSpringConfig.EXECUTOR_BEAN_NAME) - void exportPlain( - final ConfigurationFormat exportFormat, - final OutputStream out, - final Long institutionId, - final Long configurationNodeId) throws Exception { - - if (log.isDebugEnabled()) { - log.debug("Start export SEB plain XML configuration asynconously"); - } - - try { - - // get all defined root configuration attributes prepared and sorted - final List sortedAttributes = this.configurationAttributeDAO.getAllRootAttributes() - .getOrThrow() - .stream() - .flatMap(this::convertAttribute) - .filter(exportFormatBasedAttributeFilter(exportFormat)) - .sorted() - .collect(Collectors.toList()); - - // get follow-up configurationId for given configurationNodeId - final Long configurationId = this.configurationDAO - .getConfigurationLastStableVersion(configurationNodeId) - .getOrThrow().id; - - final Function configurationValueSupplier = - getConfigurationValueSupplier(institutionId, configurationId); - - writeHeader(exportFormat, out); - - // write attributes - final Iterator iterator = sortedAttributes.iterator(); - while (iterator.hasNext()) { - - final ConfigurationAttribute attribute = iterator.next(); - final AttributeValueConverter attributeValueConverter = - this.attributeValueConverterService.getAttributeValueConverter(attribute); - - switch (exportFormat) { - case XML: { - attributeValueConverter.convertToXML( - out, - attribute, - configurationValueSupplier); - break; - } - case JSON: { - attributeValueConverter.convertToJSON( - out, - attribute, - configurationValueSupplier); - if (iterator.hasNext()) { - out.write(JSON_SEPARATOR); - } - break; - } - } - } - - writeFooter(exportFormat, out); - - if (log.isDebugEnabled()) { - log.debug("Finished export SEB plain XML configuration asynconously"); - } - - } catch (final Exception e) { - log.error("Unexpected error while trying to write SEB Exam Configuration XML to output stream: ", e); - throw e; - } finally { - try { - out.flush(); - } catch (final IOException e1) { - log.error("Unable to flush output stream after error"); - } - IOUtils.closeQuietly(out); - } - } - - /** 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 */ - 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, - Function.identity())); - - // 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 ExamConfigXMLParser examConfigImportHandler = new ExamConfigXMLParser( - institutionId, - configurationId, - value -> this.configurationValueDAO - .save(value) - .getOrThrow(), - 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); - } - } - - InputStream unzip(final InputStream input) throws Exception { - - final byte[] zipHeader = new byte[Constants.GZIP_HEADER_LENGTH]; - final int read = input.read(zipHeader); - if (read < Constants.GZIP_HEADER_LENGTH) { - throw new IllegalArgumentException("Failed to verify Zip type from input stream. Header size mismatch."); - } - - final boolean isZipped = Byte.toUnsignedInt(zipHeader[0]) == Constants.GZIP_ID1 - && Byte.toUnsignedInt(zipHeader[1]) == Constants.GZIP_ID2 - && Byte.toUnsignedInt(zipHeader[2]) == Constants.GZIP_CM; - - final InputStream sequencedInput = new SequenceInputStream( - new ByteArrayInputStream(zipHeader, 0, Constants.GZIP_HEADER_LENGTH), - input); - - if (isZipped) { - - final PipedInputStream pipedIn = new PipedInputStream(); - final PipedOutputStream pipedOut = new PipedOutputStream(pipedIn); - this.zipService.read(pipedOut, sequencedInput); - - return pipedIn; - } else { - return sequencedInput; - } - } - - 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); - } - - private void writeFooter( - final ConfigurationFormat exportFormat, - final OutputStream out) throws IOException { - - if (exportFormat == ConfigurationFormat.XML) { - // plist close - out.write(XML_DICT_END_UTF_8); - out.write(XML_PLIST_END_UTF_8); - } else { - out.write(JSON_END); - } - } - - private void writeHeader( - final ConfigurationFormat exportFormat, - final OutputStream out) throws IOException { - - if (exportFormat == ConfigurationFormat.XML) { - writeXMLHeaderInformation(out); - } else { - writeJSONHeaderInformation(out); - } - } - - private void writeJSONHeaderInformation(final OutputStream out) throws IOException { - out.write(JSON_START); - } - - private void writeXMLHeaderInformation(final OutputStream out) throws IOException { - // write headers - out.write(XML_VERSION_HEADER_UTF_8); - out.write(XML_DOCTYPE_HEADER_UTF_8); - - // plist open - out.write(XML_PLIST_START_V1_UTF_8); - out.write(XML_DICT_START_UTF_8); - } - - private Stream convertAttribute(final ConfigurationAttribute attr) { - final AttributeValueConverter attributeValueConverter = - this.attributeValueConverterService.getAttributeValueConverter(attr); - if (attributeValueConverter != null) { - return attributeValueConverter.convertAttribute(attr); - } else { - return Stream.of(attr); - } - } - - private Function getConfigurationValueSupplier( - final Long institutionId, - final Long configurationId) { - - final Map mapping = this.configurationValueDAO - .allRootAttributeValues(institutionId, configurationId) - .getOrThrow() - .stream() - .collect(Collectors.toMap( - ConfigurationValue::getAttributeId, - Function.identity())); - - return attr -> mapping.get(attr.id); - } - -} +/* + * 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.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.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +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 ch.ethz.seb.sebserver.gbl.util.Cryptor; +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; +import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationAttribute; +import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationValue; +import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; +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; +import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ZipService; + +@Lazy +@Component +@WebServiceProfile +public class ExamConfigIO { + + private static final Logger log = LoggerFactory.getLogger(ExamConfigIO.class); + + private static final byte[] XML_VERSION_HEADER_UTF_8 = Utils.toByteArray(Constants.XML_VERSION_HEADER); + private static final byte[] XML_DOCTYPE_HEADER_UTF_8 = Utils.toByteArray(Constants.XML_DOCTYPE_HEADER); + private static final byte[] XML_PLIST_START_V1_UTF_8 = Utils.toByteArray(Constants.XML_PLIST_START_V1); + private static final byte[] XML_PLIST_END_UTF_8 = Utils.toByteArray(Constants.XML_PLIST_END); + private static final byte[] XML_DICT_START_UTF_8 = Utils.toByteArray(Constants.XML_DICT_START); + private static final byte[] XML_DICT_END_UTF_8 = Utils.toByteArray(Constants.XML_DICT_END); + private static final byte[] JSON_START = Utils.toByteArray("{"); + private static final byte[] JSON_END = Utils.toByteArray("}"); + private static final byte[] JSON_SEPARATOR = Utils.toByteArray(Constants.LIST_SEPARATOR); + + private final ConfigurationAttributeDAO configurationAttributeDAO; + private final ConfigurationValueDAO configurationValueDAO; + private final ConfigurationDAO configurationDAO; + private final AttributeValueConverterService attributeValueConverterService; + private final ZipService zipService; + private final Cryptor cryptor; + + protected ExamConfigIO( + final ConfigurationAttributeDAO configurationAttributeDAO, + final ConfigurationValueDAO configurationValueDAO, + final ConfigurationDAO configurationDAO, + final AttributeValueConverterService attributeValueConverterService, + final ZipService zipService, + final Cryptor cryptor) { + + this.configurationAttributeDAO = configurationAttributeDAO; + this.configurationValueDAO = configurationValueDAO; + this.configurationDAO = configurationDAO; + this.attributeValueConverterService = attributeValueConverterService; + this.zipService = zipService; + this.cryptor = cryptor; + } + + @Async(AsyncServiceSpringConfig.EXECUTOR_BEAN_NAME) + void exportPlain( + final ConfigurationFormat exportFormat, + final OutputStream out, + final Long institutionId, + final Long configurationNodeId) throws Exception { + + if (log.isDebugEnabled()) { + log.debug("Start export SEB plain XML configuration asynconously"); + } + + try { + + // get all defined root configuration attributes prepared and sorted + final List sortedAttributes = this.configurationAttributeDAO.getAllRootAttributes() + .getOrThrow() + .stream() + .flatMap(this::convertAttribute) + .filter(exportFormatBasedAttributeFilter(exportFormat)) + .sorted() + .collect(Collectors.toList()); + + // get follow-up configurationId for given configurationNodeId + final Long configurationId = this.configurationDAO + .getConfigurationLastStableVersion(configurationNodeId) + .getOrThrow().id; + + final Function configurationValueSupplier = + getConfigurationValueSupplier(institutionId, configurationId); + + writeHeader(exportFormat, out); + + // write attributes + final Iterator iterator = sortedAttributes.iterator(); + while (iterator.hasNext()) { + + final ConfigurationAttribute attribute = iterator.next(); + final AttributeValueConverter attributeValueConverter = + this.attributeValueConverterService.getAttributeValueConverter(attribute); + + switch (exportFormat) { + case XML: { + attributeValueConverter.convertToXML( + out, + attribute, + configurationValueSupplier); + break; + } + case JSON: { + attributeValueConverter.convertToJSON( + out, + attribute, + configurationValueSupplier); + if (iterator.hasNext()) { + out.write(JSON_SEPARATOR); + } + break; + } + } + } + + writeFooter(exportFormat, out); + + if (log.isDebugEnabled()) { + log.debug("Finished export SEB plain XML configuration asynconously"); + } + + } catch (final Exception e) { + log.error("Unexpected error while trying to write SEB Exam Configuration XML to output stream: ", e); + throw e; + } finally { + try { + out.flush(); + } catch (final IOException e1) { + log.error("Unable to flush output stream after error"); + } + IOUtils.closeQuietly(out); + } + } + + /** 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 */ + 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, + Function.identity())); + + // 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 ExamConfigXMLParser examConfigImportHandler = new ExamConfigXMLParser( + cryptor, + institutionId, + configurationId, + value -> this.configurationValueDAO + .save(value) + .getOrThrow(), + 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); + } + } + + InputStream unzip(final InputStream input) throws Exception { + + final byte[] zipHeader = new byte[Constants.GZIP_HEADER_LENGTH]; + final int read = input.read(zipHeader); + if (read < Constants.GZIP_HEADER_LENGTH) { + throw new IllegalArgumentException("Failed to verify Zip type from input stream. Header size mismatch."); + } + + final boolean isZipped = Byte.toUnsignedInt(zipHeader[0]) == Constants.GZIP_ID1 + && Byte.toUnsignedInt(zipHeader[1]) == Constants.GZIP_ID2 + && Byte.toUnsignedInt(zipHeader[2]) == Constants.GZIP_CM; + + final InputStream sequencedInput = new SequenceInputStream( + new ByteArrayInputStream(zipHeader, 0, Constants.GZIP_HEADER_LENGTH), + input); + + if (isZipped) { + + final PipedInputStream pipedIn = new PipedInputStream(); + final PipedOutputStream pipedOut = new PipedOutputStream(pipedIn); + this.zipService.read(pipedOut, sequencedInput); + + return pipedIn; + } else { + return sequencedInput; + } + } + + 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); + } + + private void writeFooter( + final ConfigurationFormat exportFormat, + final OutputStream out) throws IOException { + + if (exportFormat == ConfigurationFormat.XML) { + // plist close + out.write(XML_DICT_END_UTF_8); + out.write(XML_PLIST_END_UTF_8); + } else { + out.write(JSON_END); + } + } + + private void writeHeader( + final ConfigurationFormat exportFormat, + final OutputStream out) throws IOException { + + if (exportFormat == ConfigurationFormat.XML) { + writeXMLHeaderInformation(out); + } else { + writeJSONHeaderInformation(out); + } + } + + private void writeJSONHeaderInformation(final OutputStream out) throws IOException { + out.write(JSON_START); + } + + private void writeXMLHeaderInformation(final OutputStream out) throws IOException { + // write headers + out.write(XML_VERSION_HEADER_UTF_8); + out.write(XML_DOCTYPE_HEADER_UTF_8); + + // plist open + out.write(XML_PLIST_START_V1_UTF_8); + out.write(XML_DICT_START_UTF_8); + } + + private Stream convertAttribute(final ConfigurationAttribute attr) { + final AttributeValueConverter attributeValueConverter = + this.attributeValueConverterService.getAttributeValueConverter(attr); + if (attributeValueConverter != null) { + return attributeValueConverter.convertAttribute(attr); + } else { + return Stream.of(attr); + } + } + + private Function getConfigurationValueSupplier( + final Long institutionId, + final Long configurationId) { + + final Map mapping = this.configurationValueDAO + .allRootAttributeValues(institutionId, configurationId) + .getOrThrow() + .stream() + .collect(Collectors.toMap( + ConfigurationValue::getAttributeId, + Function.identity())); + + return attr -> mapping.get(attr.id); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/ExamConfigServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/ExamConfigServiceImpl.java index 36bcf565..89e65e78 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/ExamConfigServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/ExamConfigServiceImpl.java @@ -1,432 +1,432 @@ -/* - * 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.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.PipedInputStream; -import java.io.PipedOutputStream; -import java.util.Collection; -import java.util.List; -import java.util.concurrent.Future; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import org.apache.commons.codec.digest.DigestUtils; -import org.apache.commons.io.IOUtils; -import org.apache.commons.lang3.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.context.annotation.Lazy; -import org.springframework.stereotype.Service; - -import ch.ethz.seb.sebserver.gbl.api.APIMessage; -import ch.ethz.seb.sebserver.gbl.api.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; -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.ExamConfigurationMapDAO; -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.ExamConfigService; -import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.SebConfigEncryptionService; -import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.SebConfigEncryptionService.Strategy; -import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ZipService; -import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.impl.SebConfigEncryptionServiceImpl.EncryptionContext; - -@Lazy -@Service -@WebServiceProfile -public class ExamConfigServiceImpl implements ExamConfigService { - - private static final Logger log = LoggerFactory.getLogger(ExamConfigServiceImpl.class); - - private final ExamConfigIO examConfigIO; - private final ConfigurationAttributeDAO configurationAttributeDAO; - private final ExamConfigurationMapDAO examConfigurationMapDAO; - private final Collection validators; - private final ClientCredentialService clientCredentialService; - private final ZipService zipService; - private final SebConfigEncryptionService sebConfigEncryptionService; - - protected ExamConfigServiceImpl( - final ExamConfigIO examConfigIO, - final ConfigurationAttributeDAO configurationAttributeDAO, - final ExamConfigurationMapDAO examConfigurationMapDAO, - final Collection validators, - final ClientCredentialService clientCredentialService, - final ZipService zipService, - final SebConfigEncryptionService sebConfigEncryptionService) { - - this.examConfigIO = examConfigIO; - this.configurationAttributeDAO = configurationAttributeDAO; - this.examConfigurationMapDAO = examConfigurationMapDAO; - this.validators = validators; - this.clientCredentialService = clientCredentialService; - this.zipService = zipService; - this.sebConfigEncryptionService = sebConfigEncryptionService; - } - - @Override - public void validate(final ConfigurationValue value) throws FieldValidationException { - if (value == null) { - log.warn("Validate called with null reference. Ignore this and skip validation"); - return; - } - - final ConfigurationAttribute attribute = this.configurationAttributeDAO - .byPK(value.attributeId) - .getOrThrow(); - - this.validators - .stream() - .filter(validator -> !validator.validate(value, attribute)) - .findFirst() - .ifPresent(validator -> validator.throwValidationError(value, attribute)); - } - - @Override - public void validate(final ConfigurationTableValues tableValue) throws FieldValidationException { - final List errors = tableValue.values.stream() - .map(tv -> new ConfigurationValue( - null, - tableValue.institutionId, - tableValue.configurationId, - tv.attributeId, - tv.listIndex, - tv.value)) - .flatMap(cv -> { - try { - validate(cv); - return Stream.empty(); - } catch (final FieldValidationException fve) { - return Stream.of(fve); - } - }) - .map(fve -> fve.apiMessage) - .collect(Collectors.toList()); - - if (!errors.isEmpty()) { - throw new APIMessageException(errors); - } - } - - @Override - public void exportPlainXML( - final OutputStream out, - final Long institutionId, - final Long configurationNodeId) { - - this.exportPlainOnly(ConfigurationFormat.XML, out, institutionId, configurationNodeId); - } - - @Override - public void exportPlainJSON( - final OutputStream out, - final Long institutionId, - final Long configurationNodeId) { - - this.exportPlainOnly(ConfigurationFormat.JSON, out, institutionId, configurationNodeId); - } - - public Result getDefaultConfigurationIdForExam(final Long examId) { - return this.examConfigurationMapDAO.getDefaultConfigurationNode(examId); - } - - public Result getUserConfigurationIdForExam(final Long examId, final String userId) { - return this.examConfigurationMapDAO.getUserConfigurationNodeId(examId, userId); - } - - @Override - public Long exportForExam( - final OutputStream out, - final Long institutionId, - final Long examId, - final String userId) { - - final Long configurationNodeId = (StringUtils.isBlank(userId)) - ? getDefaultConfigurationIdForExam(examId) - .getOrThrow() - : getUserConfigurationIdForExam(examId, userId) - .getOrThrow(); - - return exportForExam(out, institutionId, examId, configurationNodeId); - } - - @Override - public Long exportForExam( - final OutputStream out, - final Long institutionId, - final Long examId, - final Long configurationNodeId) { - - final CharSequence passwordCipher = this.examConfigurationMapDAO - .getConfigPasswortCipher(examId, configurationNodeId) - .getOr(null); - - if (StringUtils.isNotBlank(passwordCipher)) { - - if (log.isDebugEnabled()) { - log.debug("*** Seb exam configuration with password based encryption"); - } - - final CharSequence encryptionPasswordPlaintext = this.clientCredentialService - .decrypt(passwordCipher); - - PipedOutputStream plainOut = null; - PipedInputStream zipIn = null; - - PipedOutputStream zipOut = null; - PipedInputStream cryptIn = null; - - PipedOutputStream cryptOut = null; - PipedInputStream in = null; - - try { - - plainOut = new PipedOutputStream(); - zipIn = new PipedInputStream(plainOut); - - zipOut = new PipedOutputStream(); - cryptIn = new PipedInputStream(zipOut); - - cryptOut = new PipedOutputStream(); - in = new PipedInputStream(cryptOut); - - // streaming... - // export plain text - this.examConfigIO.exportPlain( - ConfigurationFormat.XML, - plainOut, - institutionId, - configurationNodeId); - // zip the plain text - this.zipService.write(zipOut, zipIn); - // encrypt the zipped plain text - this.sebConfigEncryptionService.streamEncrypted( - cryptOut, - cryptIn, - EncryptionContext.contextOf( - Strategy.PASSWORD_PSWD, - encryptionPasswordPlaintext)); - - // copy to output - IOUtils.copyLarge(in, out); - - } catch (final Exception e) { - log.error("Error while zip and encrypt seb exam config stream: ", e); - } finally { - IOUtils.closeQuietly(zipIn); - IOUtils.closeQuietly(plainOut); - IOUtils.closeQuietly(cryptIn); - IOUtils.closeQuietly(zipOut); - IOUtils.closeQuietly(in); - IOUtils.closeQuietly(cryptOut); - } - } else { - // just export in plain text XML format - this.exportPlainXML(out, institutionId, configurationNodeId); - } - - return configurationNodeId; - } - - @Override - public Result generateConfigKey( - final Long institutionId, - final Long configurationNodeId) { - - if (log.isDebugEnabled()) { - log.debug("Start to stream plain JSON SEB Configuration data for Config-Key generation"); - } - - if (log.isTraceEnabled()) { - PipedOutputStream pout = null; - PipedInputStream pin = null; - try { - pout = new PipedOutputStream(); - pin = new PipedInputStream(pout); - this.examConfigIO.exportPlain( - ConfigurationFormat.JSON, - pout, - institutionId, - configurationNodeId); - - final String json = IOUtils.toString(pin, "UTF-8"); - - log.trace("SEB Configuration JSON to create Config-Key: {}", json); - } catch (final Exception e) { - log.error("Failed to trace SEB Configuration JSON: ", e); - } - } - - PipedOutputStream pout = null; - PipedInputStream pin = null; - try { - pout = new PipedOutputStream(); - pin = new PipedInputStream(pout); - - this.examConfigIO.exportPlain( - ConfigurationFormat.JSON, - pout, - institutionId, - configurationNodeId); - - final String configKey = DigestUtils.sha256Hex(pin); - - return Result.of(configKey); - - } catch (final Exception e) { - log.error("Error while stream plain JSON SEB Configuration data for Config-Key generation: ", e); - return Result.ofError(e); - } finally { - try { - if (pin != null) { - pin.close(); - } - } catch (final IOException e1) { - log.error("Failed to close PipedInputStream: ", e1); - } - try { - if (pout != null) { - pout.close(); - } - } catch (final IOException e1) { - log.error("Failed to close PipedOutputStream: ", e1); - } - - if (log.isDebugEnabled()) { - log.debug("Finished to stream plain JSON SEB Configuration data for Config-Key generation"); - } - } - } - - @Override - public Result> generateConfigKeys(final Long institutionId, final Long examId) { - return this.examConfigurationMapDAO.getConfigurationNodeIds(examId) - .map(ids -> ids - .stream() - .map(id -> generateConfigKey(institutionId, id) - .getOrThrow()) - .collect(Collectors.toList())); - } - - @Override - public Result importFromSEBFile( - final Configuration config, - final InputStream input, - final CharSequence password) { - - return Result.tryCatch(() -> { - - Future streamDecrypted = null; - InputStream cryptIn = null; - PipedInputStream plainIn = null; - PipedOutputStream cryptOut = null; - InputStream unzippedIn = null; - try { - - cryptIn = this.examConfigIO.unzip(input); - plainIn = new PipedInputStream(); - cryptOut = new PipedOutputStream(plainIn); - - // decrypt - streamDecrypted = this.sebConfigEncryptionService.streamDecrypted( - cryptOut, - cryptIn, - EncryptionContext.contextOf(password)); - - // if zipped, unzip attach unzip stream first - unzippedIn = this.examConfigIO.unzip(plainIn); - - // parse XML and import - this.examConfigIO.importPlainXML( - unzippedIn, - config.institutionId, - config.id); - - return config; - - } catch (final Exception e) { - log.error("Unexpected error while trying to import SEB Exam Configuration: ", e); - - if (streamDecrypted != null) { - final Exception exception = streamDecrypted.get(); - if (exception != null && exception instanceof APIMessageException) { - throw exception; - } - } - - throw new RuntimeException("Failed to import SEB configuration. Cause is: " + e.getMessage()); - } finally { - IOUtils.closeQuietly(cryptIn); - IOUtils.closeQuietly(plainIn); - IOUtils.closeQuietly(cryptOut); - IOUtils.closeQuietly(unzippedIn); - } - }); - } - - private void exportPlainOnly( - final ConfigurationFormat exportFormat, - final OutputStream out, - final Long institutionId, - final Long configurationNodeId) { - - if (log.isDebugEnabled()) { - log.debug("Start to stream plain text SEB Configuration data"); - } - - PipedOutputStream pout = null; - PipedInputStream pin = null; - try { - pout = new PipedOutputStream(); - pin = new PipedInputStream(pout); - - this.examConfigIO.exportPlain( - exportFormat, - pout, - institutionId, - configurationNodeId); - - IOUtils.copyLarge(pin, out); - - } catch (final Exception e) { - log.error("Error while stream plain text SEB Configuration export data: ", e); - } finally { - try { - if (pin != null) { - pin.close(); - } - } catch (final IOException e1) { - log.error("Failed to close PipedInputStream: ", e1); - } - try { - if (pout != null) { - pout.flush(); - pout.close(); - } - } catch (final IOException e1) { - log.error("Failed to close PipedOutputStream: ", e1); - } - - if (log.isDebugEnabled()) { - log.debug("Finished to stream plain text SEB Configuration export data"); - } - } - } - -} +/* + * 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.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.Future; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; + +import ch.ethz.seb.sebserver.gbl.api.APIMessage; +import ch.ethz.seb.sebserver.gbl.api.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; +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.ExamConfigurationMapDAO; +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.ExamConfigService; +import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.SebConfigEncryptionService; +import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.SebConfigEncryptionService.Strategy; +import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ZipService; +import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.impl.SebConfigEncryptionServiceImpl.EncryptionContext; + +@Lazy +@Service +@WebServiceProfile +public class ExamConfigServiceImpl implements ExamConfigService { + + private static final Logger log = LoggerFactory.getLogger(ExamConfigServiceImpl.class); + + private final ExamConfigIO examConfigIO; + private final ConfigurationAttributeDAO configurationAttributeDAO; + private final ExamConfigurationMapDAO examConfigurationMapDAO; + private final Collection validators; + private final ClientCredentialService clientCredentialService; + private final ZipService zipService; + private final SebConfigEncryptionService sebConfigEncryptionService; + + protected ExamConfigServiceImpl( + final ExamConfigIO examConfigIO, + final ConfigurationAttributeDAO configurationAttributeDAO, + final ExamConfigurationMapDAO examConfigurationMapDAO, + final Collection validators, + final ClientCredentialService clientCredentialService, + final ZipService zipService, + final SebConfigEncryptionService sebConfigEncryptionService) { + + this.examConfigIO = examConfigIO; + this.configurationAttributeDAO = configurationAttributeDAO; + this.examConfigurationMapDAO = examConfigurationMapDAO; + this.validators = validators; + this.clientCredentialService = clientCredentialService; + this.zipService = zipService; + this.sebConfigEncryptionService = sebConfigEncryptionService; + } + + @Override + public void validate(final ConfigurationValue value) throws FieldValidationException { + if (value == null) { + log.warn("Validate called with null reference. Ignore this and skip validation"); + return; + } + + final ConfigurationAttribute attribute = this.configurationAttributeDAO + .byPK(value.attributeId) + .getOrThrow(); + + this.validators + .stream() + .filter(validator -> !validator.validate(value, attribute)) + .findFirst() + .ifPresent(validator -> validator.throwValidationError(value, attribute)); + } + + @Override + public void validate(final ConfigurationTableValues tableValue) throws FieldValidationException { + final List errors = tableValue.values.stream() + .map(tv -> new ConfigurationValue( + null, + tableValue.institutionId, + tableValue.configurationId, + tv.attributeId, + tv.listIndex, + tv.value)) + .flatMap(cv -> { + try { + validate(cv); + return Stream.empty(); + } catch (final FieldValidationException fve) { + return Stream.of(fve); + } + }) + .map(fve -> fve.apiMessage) + .collect(Collectors.toList()); + + if (!errors.isEmpty()) { + throw new APIMessageException(errors); + } + } + + @Override + public void exportPlainXML( + final OutputStream out, + final Long institutionId, + final Long configurationNodeId) { + + this.exportPlainOnly(ConfigurationFormat.XML, out, institutionId, configurationNodeId); + } + + @Override + public void exportPlainJSON( + final OutputStream out, + final Long institutionId, + final Long configurationNodeId) { + + this.exportPlainOnly(ConfigurationFormat.JSON, out, institutionId, configurationNodeId); + } + + public Result getDefaultConfigurationIdForExam(final Long examId) { + return this.examConfigurationMapDAO.getDefaultConfigurationNode(examId); + } + + public Result getUserConfigurationIdForExam(final Long examId, final String userId) { + return this.examConfigurationMapDAO.getUserConfigurationNodeId(examId, userId); + } + + @Override + public Long exportForExam( + final OutputStream out, + final Long institutionId, + final Long examId, + final String userId) { + + final Long configurationNodeId = (StringUtils.isBlank(userId)) + ? getDefaultConfigurationIdForExam(examId) + .getOrThrow() + : getUserConfigurationIdForExam(examId, userId) + .getOrThrow(); + + return exportForExam(out, institutionId, examId, configurationNodeId); + } + + @Override + public Long exportForExam( + final OutputStream out, + final Long institutionId, + final Long examId, + final Long configurationNodeId) { + + final CharSequence passwordCipher = this.examConfigurationMapDAO + .getConfigPasswordCipher(examId, configurationNodeId) + .getOr(null); + + if (StringUtils.isNotBlank(passwordCipher)) { + + if (log.isDebugEnabled()) { + log.debug("*** Seb exam configuration with password based encryption"); + } + + final CharSequence encryptionPasswordPlaintext = this.clientCredentialService + .decrypt(passwordCipher); + + PipedOutputStream plainOut = null; + PipedInputStream zipIn = null; + + PipedOutputStream zipOut = null; + PipedInputStream cryptIn = null; + + PipedOutputStream cryptOut = null; + PipedInputStream in = null; + + try { + + plainOut = new PipedOutputStream(); + zipIn = new PipedInputStream(plainOut); + + zipOut = new PipedOutputStream(); + cryptIn = new PipedInputStream(zipOut); + + cryptOut = new PipedOutputStream(); + in = new PipedInputStream(cryptOut); + + // streaming... + // export plain text + this.examConfigIO.exportPlain( + ConfigurationFormat.XML, + plainOut, + institutionId, + configurationNodeId); + // zip the plain text + this.zipService.write(zipOut, zipIn); + // encrypt the zipped plain text + this.sebConfigEncryptionService.streamEncrypted( + cryptOut, + cryptIn, + EncryptionContext.contextOf( + Strategy.PASSWORD_PSWD, + encryptionPasswordPlaintext)); + + // copy to output + IOUtils.copyLarge(in, out); + + } catch (final Exception e) { + log.error("Error while zip and encrypt seb exam config stream: ", e); + } finally { + IOUtils.closeQuietly(zipIn); + IOUtils.closeQuietly(plainOut); + IOUtils.closeQuietly(cryptIn); + IOUtils.closeQuietly(zipOut); + IOUtils.closeQuietly(in); + IOUtils.closeQuietly(cryptOut); + } + } else { + // just export in plain text XML format + this.exportPlainXML(out, institutionId, configurationNodeId); + } + + return configurationNodeId; + } + + @Override + public Result generateConfigKey( + final Long institutionId, + final Long configurationNodeId) { + + if (log.isDebugEnabled()) { + log.debug("Start to stream plain JSON SEB Configuration data for Config-Key generation"); + } + + if (log.isTraceEnabled()) { + PipedOutputStream pout = null; + PipedInputStream pin = null; + try { + pout = new PipedOutputStream(); + pin = new PipedInputStream(pout); + this.examConfigIO.exportPlain( + ConfigurationFormat.JSON, + pout, + institutionId, + configurationNodeId); + + final String json = IOUtils.toString(pin, "UTF-8"); + + log.trace("SEB Configuration JSON to create Config-Key: {}", json); + } catch (final Exception e) { + log.error("Failed to trace SEB Configuration JSON: ", e); + } + } + + PipedOutputStream pout = null; + PipedInputStream pin = null; + try { + pout = new PipedOutputStream(); + pin = new PipedInputStream(pout); + + this.examConfigIO.exportPlain( + ConfigurationFormat.JSON, + pout, + institutionId, + configurationNodeId); + + final String configKey = DigestUtils.sha256Hex(pin); + + return Result.of(configKey); + + } catch (final Exception e) { + log.error("Error while stream plain JSON SEB Configuration data for Config-Key generation: ", e); + return Result.ofError(e); + } finally { + try { + if (pin != null) { + pin.close(); + } + } catch (final IOException e1) { + log.error("Failed to close PipedInputStream: ", e1); + } + try { + if (pout != null) { + pout.close(); + } + } catch (final IOException e1) { + log.error("Failed to close PipedOutputStream: ", e1); + } + + if (log.isDebugEnabled()) { + log.debug("Finished to stream plain JSON SEB Configuration data for Config-Key generation"); + } + } + } + + @Override + public Result> generateConfigKeys(final Long institutionId, final Long examId) { + return this.examConfigurationMapDAO.getConfigurationNodeIds(examId) + .map(ids -> ids + .stream() + .map(id -> generateConfigKey(institutionId, id) + .getOrThrow()) + .collect(Collectors.toList())); + } + + @Override + public Result importFromSEBFile( + final Configuration config, + final InputStream input, + final CharSequence password) { + + return Result.tryCatch(() -> { + + Future streamDecrypted = null; + InputStream cryptIn = null; + PipedInputStream plainIn = null; + PipedOutputStream cryptOut = null; + InputStream unzippedIn = null; + try { + + cryptIn = this.examConfigIO.unzip(input); + plainIn = new PipedInputStream(); + cryptOut = new PipedOutputStream(plainIn); + + // decrypt + streamDecrypted = this.sebConfigEncryptionService.streamDecrypted( + cryptOut, + cryptIn, + EncryptionContext.contextOf(password)); + + // if zipped, unzip attach unzip stream first + unzippedIn = this.examConfigIO.unzip(plainIn); + + // parse XML and import + this.examConfigIO.importPlainXML( + unzippedIn, + config.institutionId, + config.id); + + return config; + + } catch (final Exception e) { + log.error("Unexpected error while trying to import SEB Exam Configuration: ", e); + + if (streamDecrypted != null) { + final Exception exception = streamDecrypted.get(); + if (exception != null && exception instanceof APIMessageException) { + throw exception; + } + } + + throw new RuntimeException("Failed to import SEB configuration. Cause is: " + e.getMessage()); + } finally { + IOUtils.closeQuietly(cryptIn); + IOUtils.closeQuietly(plainIn); + IOUtils.closeQuietly(cryptOut); + IOUtils.closeQuietly(unzippedIn); + } + }); + } + + private void exportPlainOnly( + final ConfigurationFormat exportFormat, + final OutputStream out, + final Long institutionId, + final Long configurationNodeId) { + + if (log.isDebugEnabled()) { + log.debug("Start to stream plain text SEB Configuration data"); + } + + PipedOutputStream pout = null; + PipedInputStream pin = null; + try { + pout = new PipedOutputStream(); + pin = new PipedInputStream(pout); + + this.examConfigIO.exportPlain( + exportFormat, + pout, + institutionId, + configurationNodeId); + + IOUtils.copyLarge(pin, out); + + } catch (final Exception e) { + log.error("Error while stream plain text SEB Configuration export data: ", e); + } finally { + try { + if (pin != null) { + pin.close(); + } + } catch (final IOException e1) { + log.error("Failed to close PipedInputStream: ", e1); + } + try { + if (pout != null) { + pout.flush(); + pout.close(); + } + } catch (final IOException e1) { + log.error("Failed to close PipedOutputStream: ", e1); + } + + if (log.isDebugEnabled()) { + log.debug("Finished to stream plain text SEB Configuration export data"); + } + } + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/ExamConfigXMLParser.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/ExamConfigXMLParser.java index 247f53de..f726d974 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/ExamConfigXMLParser.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/ExamConfigXMLParser.java @@ -1,532 +1,558 @@ -/* - * 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.apache.commons.lang3.BooleanUtils; -import org.apache.commons.lang3.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -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.AttributeType; -import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationAttribute; -import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationValue; -import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.impl.ExamConfigXMLParser.PListNode.Type; -import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.impl.converter.KioskModeConverter; - -public class ExamConfigXMLParser extends DefaultHandler { - - private static final Logger log = LoggerFactory.getLogger(ExamConfigXMLParser.class); - - // comma separated list of SEB exam config keys that can be ignored on imports - // See: https://jira.let.ethz.ch/browse/SEBSERV-100 - private static final Set SEB_EXAM_CONFIG_KEYS_TO_IGNORE = new HashSet<>(Arrays.asList( - // SEB Server specific - "sebMode", - "sebServerFallback", - "sebServerURL", - - // Obsolete on SEB Server - "startURL", - "startURLAllowDeepLink", - "startURLAppendQueryParameter", - - // These keys don't exist anymore: - "examConfigKeyContainedKeys", - "allowWLAN", - "insideSebEnableEnableNetworkConnectionSelector", - "ignoreQuitPassword", - "oskBehavior", - "outsideSebEnableChangeAPassword", - "outsideSebEnableEaseOfAccess", - "outsideSebEnableLockThisComputer", - "outsideSebEnableLogOff", - "outsideSebEnableShutDownurlFilterRegex", - "outsideSebEnableStartTaskManager", - "outsideSebEnableSwitchUser", - "outsideSebEnableVmWareClientShade", - "outsideSebEnableShutDown", - "enableURLContentFilter", - "enableURLFilter", - "prohibitedProcesses.windowHandlingProcess", - "permittedProcesses.windowHandlingProcess", - "backgroundOpenSEBConfig", - - // These keys are only used internally - "urlFilterRegex", - "urlFilterTrustedContent", - "blacklistURLFilter", - "whitelistURLFilter", - "URLFilterIgnoreList")); - - 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_DATA, - Constants.XML_PLIST_INTEGER)); - - private static final Set KNOWN_INLINE_TABLES = new HashSet<>(Arrays.asList( - "arguments")); - - private final Consumer valueConsumer; - private final Function attributeResolver; - private final Long institutionId; - private final Long configId; - - private final Stack stack = new Stack<>(); - - private Boolean killExplorerShell = null; - private Boolean createNewDesktop = null; - - public ExamConfigXMLParser( - final Long institutionId, - final Long configId, - final Consumer valueConsumer, - final Function attributeResolver) { - - super(); - this.valueConsumer = valueConsumer; - this.attributeResolver = attributeResolver; - this.institutionId = institutionId; - this.configId = configId; - } - - @Override - public void startDocument() throws SAXException { - if (log.isDebugEnabled()) { - log.debug("Start parsing document"); - } - } - - @Override - public void endDocument() throws SAXException { - if (log.isDebugEnabled()) { - log.debug("End parsing document"); - } - } - - @Override - public void startElement( - final String uri, - final String localName, - final String qName, - final Attributes attributes) throws SAXException { - - if (log.isDebugEnabled()) { - log.debug("start element: {}", qName); - } - - 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_DATA: - 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.inlineTable = isInlineTable(top.name); - array.name = top.name; - array.listIndex = top.listIndex; - this.stack.pop(); - this.stack.push(array); - break; - } - default: - throw new IllegalStateException(); - } - } - - private boolean isInlineTable(final String name) { - return KNOWN_INLINE_TABLES.contains(name); - } - - 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); - - // if we are in a values-array - if (parent.type == Type.ARRAY) { - if (StringUtils.isBlank(parent.value)) { - parent.value = top.value; - } else { - parent.value += "," + top.value; - } - return; - } - - // if we are in an inline table array - if (grandParent.type == Type.ARRAY && grandParent.inlineTable) { - if (StringUtils.isBlank(grandParent.value)) { - grandParent.value = top.value; - } else { - grandParent.value += "," + top.value; - } - if (StringUtils.isBlank(grandParent.valueName)) { - grandParent.valueName = top.name; - } else { - grandParent.valueName += "," + top.name; - } - return; - } - - final String attrName = (parent.type == Type.DICT && grandParent.type == Type.ARRAY) - ? parent.name + "." + top.name - : top.name; - - final ConfigurationAttribute attribute = this.attributeResolver.apply(attrName); - saveValue(attrName, attribute, top.listIndex, top.value); - } - } else if (top.type == Type.ARRAY) { - 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; - final ConfigurationAttribute attribute = this.attributeResolver.apply(attrName); - - if (top.inlineTable) { - createInlineTableValue(top, attrName, attribute); - return; - } - - // check if we have a simple values array - if (attribute != null && (attribute.type == AttributeType.MULTI_CHECKBOX_SELECTION - || attribute.type == AttributeType.MULTI_SELECTION)) { - - saveValue(attrName, attribute, top.listIndex, (top.value == null) ? "" : top.value); - } - - } else if (!Constants.XML_PLIST_KEY_NAME.equals(qName)) { - this.stack.pop(); - } - } - - private void createInlineTableValue( - final PListNode top, - final String attrName, - final ConfigurationAttribute attribute) { - - // no or blank value - if (StringUtils.isBlank(top.value)) { - saveValue(attrName, attribute, top.listIndex, null); - return; - } - - final String[] names = StringUtils.split(top.valueName, Constants.LIST_SEPARATOR); - final String[] values = StringUtils.split(top.value, Constants.LIST_SEPARATOR); - final String[] columns = StringUtils.split(attribute.getResources(), Constants.EMBEDDED_LIST_SEPARATOR); - final int numColumns = columns.length; - if (names.length != values.length) { - throw new IllegalArgumentException( - "Failed to get InlineTable values. value/name array length mismatch"); - } - - final StringBuilder valueBuilder = new StringBuilder(); - for (int i = 0; i < names.length; i++) { - if (i != 0) { - if (i % numColumns == 0) { - valueBuilder.append(Constants.LIST_SEPARATOR); - } else { - valueBuilder.append(Constants.EMBEDDED_LIST_SEPARATOR); - } - } - valueBuilder - .append(names[i]) - .append(Constants.FORM_URL_ENCODED_NAME_VALUE_SEPARATOR) - .append(values[i]); - } - - saveValue(attrName, attribute, top.listIndex, valueBuilder.toString()); - } - - @Override - public void characters( - final char[] ch, - final int start, - final int length) throws SAXException { - - final char[] valueChar = new char[length]; - System.arraycopy(ch, start, valueChar, 0, length); - final String value = String.valueOf(valueChar); - final PListNode top = this.stack.peek(); - if (top.type == Type.VALUE_STRING) { - top.value = value; - } else if (top.type == Type.VALUE_INTEGER) { - top.value = value; - } else if (top.type == Type.KEY) { - top.name = value; - } - } - - private void saveValue( - final String name, - final ConfigurationAttribute attribute, - final int listIndex, - final String value) { - - final ConfigurationValue configurationValue = createConfigurationValue( - name, - attribute, - listIndex, - value); - - if (configurationValue != null) { - if (log.isDebugEnabled()) { - log.debug("Put value: {} : {}", name, configurationValue); - } - - this.valueConsumer.accept(configurationValue); - } - } - - private ConfigurationValue createConfigurationValue( - final String name, - final ConfigurationAttribute attribute, - final int listIndex, - final String value) { - - if (attribute == null) { - if (KioskModeConverter.NAMES.contains(name)) { - return handleKioskMode(name, listIndex, value); - } - - if (SEB_EXAM_CONFIG_KEYS_TO_IGNORE.contains(name)) { - log.debug("Black-listed attribute. name={} value={}", name, value); - } else { - log.warn("Unknown attribute. name={} value={}", name, value); - } - return null; - } - - return new ConfigurationValue( - null, - this.institutionId, - this.configId, - attribute.id, - listIndex, - value); - } - - private ConfigurationValue handleKioskMode(final String name, final int listIndex, final String value) { - if (KioskModeConverter.ATTR_NAME_KILL_SHELL.equals(name)) { - this.killExplorerShell = BooleanUtils.toBoolean(value); - } else if (KioskModeConverter.ATTR_NAME_CREATE_NEW_DESKTOP.equals(name)) { - this.createNewDesktop = BooleanUtils.toBoolean(value); - } - - if (this.killExplorerShell != null && this.createNewDesktop != null) { - final ConfigurationAttribute kioskMode = this.attributeResolver.apply( - KioskModeConverter.ATTR_NAME_KIOSK_MODE); - - final String val = (this.createNewDesktop) - ? "0" - : (this.killExplorerShell) - ? "1" - : "2"; - - return new ConfigurationValue( - null, - this.institutionId, - this.configId, - kioskMode.id, - listIndex, - val); - } - - return null; - } - - 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_DATA(true, Constants.XML_PLIST_DATA), - 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; - boolean inlineTable = false; - String name; - int arrayCounter = 0; - int listIndex = 0; - String valueName; - 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(); - } - } - -} +/* + * 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 ch.ethz.seb.sebserver.gbl.util.Cryptor; +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +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.AttributeType; +import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationAttribute; +import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationValue; +import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.impl.ExamConfigXMLParser.PListNode.Type; +import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.impl.converter.KioskModeConverter; + +public class ExamConfigXMLParser extends DefaultHandler { + + private static final Logger log = LoggerFactory.getLogger(ExamConfigXMLParser.class); + + // comma separated list of SEB exam config keys that can be ignored on imports + // See: https://jira.let.ethz.ch/browse/SEBSERV-100 + private static final Set SEB_EXAM_CONFIG_KEYS_TO_IGNORE = new HashSet<>(Arrays.asList( + // SEB Server specific + "sebMode", + "sebServerFallback", + "sebServerURL", + + // Obsolete on SEB Server + "startURL", + "startURLAllowDeepLink", + "startURLAppendQueryParameter", + + // These keys don't exist anymore: + "examConfigKeyContainedKeys", + "allowWLAN", + "insideSebEnableEnableNetworkConnectionSelector", + "ignoreQuitPassword", + "oskBehavior", + "outsideSebEnableChangeAPassword", + "outsideSebEnableEaseOfAccess", + "outsideSebEnableLockThisComputer", + "outsideSebEnableLogOff", + "outsideSebEnableShutDownurlFilterRegex", + "outsideSebEnableStartTaskManager", + "outsideSebEnableSwitchUser", + "outsideSebEnableVmWareClientShade", + "outsideSebEnableShutDown", + "enableURLContentFilter", + "enableURLFilter", + "prohibitedProcesses.windowHandlingProcess", + "permittedProcesses.windowHandlingProcess", + "backgroundOpenSEBConfig", + + // These keys are only used internally + "urlFilterRegex", + "urlFilterTrustedContent", + "blacklistURLFilter", + "whitelistURLFilter", + "URLFilterIgnoreList")); + + 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_DATA, + Constants.XML_PLIST_INTEGER)); + + private static final Set KNOWN_INLINE_TABLES = new HashSet<>(Arrays.asList( + "arguments")); + + public static final Set PASSWORD_ATTRIBUTES = new HashSet<>(Arrays.asList( + "hashedQuitPassword", + "hashedAdminPassword" + )); + + public static final String IMPORTED_PASSWORD_MARKER = "_IMPORTED_PASSWORD"; + + private final Cryptor cryptor; + private final Consumer valueConsumer; + private final Function attributeResolver; + private final Long institutionId; + private final Long configId; + + private final Stack stack = new Stack<>(); + + private Boolean killExplorerShell = null; + private Boolean createNewDesktop = null; + + public ExamConfigXMLParser( + final Cryptor cryptor, + final Long institutionId, + final Long configId, + final Consumer valueConsumer, + final Function attributeResolver) { + + super(); + this.cryptor = cryptor; + this.valueConsumer = valueConsumer; + this.attributeResolver = attributeResolver; + this.institutionId = institutionId; + this.configId = configId; + } + + @Override + public void startDocument() throws SAXException { + if (log.isDebugEnabled()) { + log.debug("Start parsing document"); + } + } + + @Override + public void endDocument() throws SAXException { + if (log.isDebugEnabled()) { + log.debug("End parsing document"); + } + } + + @Override + public void startElement( + final String uri, + final String localName, + final String qName, + final Attributes attributes) throws SAXException { + + if (log.isDebugEnabled()) { + log.debug("start element: {}", qName); + } + + 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_DATA: + 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.inlineTable = isInlineTable(top.name); + array.name = top.name; + array.listIndex = top.listIndex; + this.stack.pop(); + this.stack.push(array); + break; + } + default: + throw new IllegalStateException(); + } + } + + private boolean isInlineTable(final String name) { + return KNOWN_INLINE_TABLES.contains(name); + } + + 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); + + // if we are in a values-array + if (parent.type == Type.ARRAY) { + if (StringUtils.isBlank(parent.value)) { + parent.value = top.value; + } else { + parent.value += "," + top.value; + } + return; + } + + // if we are in an inline table array + if (grandParent.type == Type.ARRAY && grandParent.inlineTable) { + if (StringUtils.isBlank(grandParent.value)) { + grandParent.value = top.value; + } else { + grandParent.value += "," + top.value; + } + if (StringUtils.isBlank(grandParent.valueName)) { + grandParent.valueName = top.name; + } else { + grandParent.valueName += "," + top.name; + } + return; + } + + final String attrName = (parent.type == Type.DICT && grandParent.type == Type.ARRAY) + ? parent.name + "." + top.name + : top.name; + + final ConfigurationAttribute attribute = this.attributeResolver.apply(attrName); + saveValue(attrName, attribute, top.listIndex, top.value); + } + } else if (top.type == Type.ARRAY) { + 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; + final ConfigurationAttribute attribute = this.attributeResolver.apply(attrName); + + if (top.inlineTable) { + createInlineTableValue(top, attrName, attribute); + return; + } + + // check if we have a simple values array + if (attribute != null && (attribute.type == AttributeType.MULTI_CHECKBOX_SELECTION + || attribute.type == AttributeType.MULTI_SELECTION)) { + + saveValue(attrName, attribute, top.listIndex, (top.value == null) ? "" : top.value); + } + + } else if (!Constants.XML_PLIST_KEY_NAME.equals(qName)) { + this.stack.pop(); + } + } + + private void createInlineTableValue( + final PListNode top, + final String attrName, + final ConfigurationAttribute attribute) { + + // no or blank value + if (StringUtils.isBlank(top.value)) { + saveValue(attrName, attribute, top.listIndex, null); + return; + } + + final String[] names = StringUtils.split(top.valueName, Constants.LIST_SEPARATOR); + final String[] values = StringUtils.split(top.value, Constants.LIST_SEPARATOR); + final String[] columns = StringUtils.split(attribute.getResources(), Constants.EMBEDDED_LIST_SEPARATOR); + final int numColumns = columns.length; + if (names.length != values.length) { + throw new IllegalArgumentException( + "Failed to get InlineTable values. value/name array length mismatch"); + } + + final StringBuilder valueBuilder = new StringBuilder(); + for (int i = 0; i < names.length; i++) { + if (i != 0) { + if (i % numColumns == 0) { + valueBuilder.append(Constants.LIST_SEPARATOR); + } else { + valueBuilder.append(Constants.EMBEDDED_LIST_SEPARATOR); + } + } + valueBuilder + .append(names[i]) + .append(Constants.FORM_URL_ENCODED_NAME_VALUE_SEPARATOR) + .append(values[i]); + } + + saveValue(attrName, attribute, top.listIndex, valueBuilder.toString()); + } + + @Override + public void characters( + final char[] ch, + final int start, + final int length) throws SAXException { + + final char[] valueChar = new char[length]; + System.arraycopy(ch, start, valueChar, 0, length); + final String value = String.valueOf(valueChar); + final PListNode top = this.stack.peek(); + if (top.type == Type.VALUE_STRING) { + top.value = value; + } else if (top.type == Type.VALUE_INTEGER) { + top.value = value; + } else if (top.type == Type.KEY) { + top.name = value; + } + } + + private void saveValue( + final String name, + final ConfigurationAttribute attribute, + final int listIndex, + final String value) { + + final ConfigurationValue configurationValue = createConfigurationValue( + name, + attribute, + listIndex, + value); + + if (configurationValue != null) { + if (log.isDebugEnabled()) { + log.debug("Put value: {} : {}", name, configurationValue); + } + + this.valueConsumer.accept(configurationValue); + } + } + + private ConfigurationValue createConfigurationValue( + final String name, + final ConfigurationAttribute attribute, + final int listIndex, + final String value) { + + if (attribute == null) { + if (KioskModeConverter.NAMES.contains(name)) { + return handleKioskMode(name, listIndex, value); + } + + if (SEB_EXAM_CONFIG_KEYS_TO_IGNORE.contains(name)) { + log.debug("Black-listed attribute. name={} value={}", name, value); + } else { + log.warn("Unknown attribute. name={} value={}", name, value); + } + return null; + } + + if (PASSWORD_ATTRIBUTES.contains(name)) { + // NOTE this is a special case, if a hashed password is imported it is not possible to view this password + // later in plain text to the administrator. Therefore this password hash is marked here as imported + // and internally encrypted as usual. So the password will be decrypted while viewing and is recognizable + // for the export so that the password can be decrypted with internal encryption and then, if import + // marked, just send to the export by removing the marker and do not rehash the already hashed password. + return new ConfigurationValue( + null, + this.institutionId, + this.configId, + attribute.id, + listIndex, + StringUtils.isNotBlank(value) ? cryptor.encrypt(value + IMPORTED_PASSWORD_MARKER).toString() : value); + } + + return new ConfigurationValue( + null, + this.institutionId, + this.configId, + attribute.id, + listIndex, + value); + } + + private ConfigurationValue handleKioskMode(final String name, final int listIndex, final String value) { + if (KioskModeConverter.ATTR_NAME_KILL_SHELL.equals(name)) { + this.killExplorerShell = BooleanUtils.toBoolean(value); + } else if (KioskModeConverter.ATTR_NAME_CREATE_NEW_DESKTOP.equals(name)) { + this.createNewDesktop = BooleanUtils.toBoolean(value); + } + + if (this.killExplorerShell != null && this.createNewDesktop != null) { + final ConfigurationAttribute kioskMode = this.attributeResolver.apply( + KioskModeConverter.ATTR_NAME_KIOSK_MODE); + + final String val = (this.createNewDesktop) + ? "0" + : (this.killExplorerShell) + ? "1" + : "2"; + + return new ConfigurationValue( + null, + this.institutionId, + this.configId, + kioskMode.id, + listIndex, + val); + } + + return null; + } + + 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_DATA(true, Constants.XML_PLIST_DATA), + 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; + boolean inlineTable = false; + String name; + int arrayCounter = 0; + int listIndex = 0; + String valueName; + 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/converter/StringConverter.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/converter/StringConverter.java index 1430f521..2817a3f2 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/converter/StringConverter.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/converter/StringConverter.java @@ -1,100 +1,133 @@ -/* - * 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.converter; - -import java.io.IOException; -import java.io.OutputStream; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; -import java.util.function.Function; - -import org.apache.commons.lang3.StringUtils; -import org.springframework.context.annotation.Lazy; -import org.springframework.stereotype.Component; - -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.ConfigurationValue; -import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; -import ch.ethz.seb.sebserver.gbl.util.Utils; -import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.AttributeValueConverter; - -@Lazy -@Component -@WebServiceProfile -public class StringConverter implements AttributeValueConverter { - - public static final Set SUPPORTED_TYPES = Collections.unmodifiableSet( - new HashSet<>(Arrays.asList( - AttributeType.TEXT_FIELD, - AttributeType.TEXT_AREA, - AttributeType.PASSWORD_FIELD, - AttributeType.DECIMAL, - AttributeType.COMBO_SELECTION))); - - private static final String XML_TEMPLATE = "%s%s"; - private static final String XML_TEMPLATE_EMPTY = "%s"; - - private static final String JSON_TEMPLATE = "\"%s\":\"%s\""; - private static final String JSON_TEMPLATE_EMPTY = "\"%s\":\"\""; - - @Override - public Set types() { - return SUPPORTED_TYPES; - } - - @Override - public void convertToXML( - final OutputStream out, - final ConfigurationAttribute attribute, - final Function valueSupplier) throws IOException { - - convert( - out, - attribute, - valueSupplier.apply(attribute), - XML_TEMPLATE, XML_TEMPLATE_EMPTY); - } - - @Override - public void convertToJSON( - final OutputStream out, - final ConfigurationAttribute attribute, - final Function valueSupplier) throws IOException { - - convert( - out, - attribute, - valueSupplier.apply(attribute), - JSON_TEMPLATE, JSON_TEMPLATE_EMPTY); - } - - private void convert( - final OutputStream out, - final ConfigurationAttribute attribute, - final ConfigurationValue value, - final String template, - final String emptyTemplate) throws IOException { - - final String val = (value != null && value.value != null) ? value.value : attribute.getDefaultValue(); - if (StringUtils.isNotBlank(val)) { - out.write(Utils.toByteArray(String.format( - template, - AttributeValueConverter.extractName(attribute), - val))); - } else { - out.write(Utils.toByteArray(String.format( - emptyTemplate, - AttributeValueConverter.extractName(attribute)))); - } - } - -} +/* + * 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.converter; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Function; + +import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentialService; +import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.impl.ExamConfigXMLParser; +import org.apache.commons.lang3.StringUtils; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +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.ConfigurationValue; +import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; +import ch.ethz.seb.sebserver.gbl.util.Utils; +import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.AttributeValueConverter; + +@Lazy +@Component +@WebServiceProfile +public class StringConverter implements AttributeValueConverter { + + public static final Set SUPPORTED_TYPES = Collections.unmodifiableSet( + new HashSet<>(Arrays.asList( + AttributeType.TEXT_FIELD, + AttributeType.TEXT_AREA, + AttributeType.PASSWORD_FIELD, + AttributeType.DECIMAL, + AttributeType.COMBO_SELECTION))); + + + + private static final String XML_TEMPLATE = "%s%s"; + private static final String XML_TEMPLATE_EMPTY = "%s"; + + private static final String JSON_TEMPLATE = "\"%s\":\"%s\""; + private static final String JSON_TEMPLATE_EMPTY = "\"%s\":\"\""; + + private final ClientCredentialService clientCredentialService; + + public StringConverter(final ClientCredentialService clientCredentialService) { + this.clientCredentialService = clientCredentialService; + } + + @Override + public Set types() { + return SUPPORTED_TYPES; + } + + @Override + public void convertToXML( + final OutputStream out, + final ConfigurationAttribute attribute, + final Function valueSupplier) throws IOException { + + convert( + out, + attribute, + valueSupplier.apply(attribute), + XML_TEMPLATE, XML_TEMPLATE_EMPTY); + } + + @Override + public void convertToJSON( + final OutputStream out, + final ConfigurationAttribute attribute, + final Function valueSupplier) throws IOException { + + convert( + out, + attribute, + valueSupplier.apply(attribute), + JSON_TEMPLATE, JSON_TEMPLATE_EMPTY); + } + + private void convert( + final OutputStream out, + final ConfigurationAttribute attribute, + final ConfigurationValue value, + final String template, + final String emptyTemplate) throws IOException { + + final String val = (value != null && value.value != null) ? value.value : attribute.getDefaultValue(); + String realName = AttributeValueConverter.extractName(attribute); + if (StringUtils.isNotBlank(val)) { + out.write(Utils.toByteArray(String.format( + template, + realName, + convertPassword(realName, val)))); + } else { + out.write(Utils.toByteArray(String.format( + emptyTemplate, + realName))); + } + } + + private CharSequence convertPassword( + final String attributeName, + final String value) { + + if (StringUtils.isBlank(value)) { + return value; + } + + if (!ExamConfigXMLParser.PASSWORD_ATTRIBUTES.contains(attributeName)) { + return value; + } + + // decrypt internally encrypted password and hash it for export + // NOTE: see special case description in ExamConfigXMLParser.createConfigurationValue + String plainText = this.clientCredentialService.decrypt(value).toString(); + if (plainText.endsWith(ExamConfigXMLParser.IMPORTED_PASSWORD_MARKER)) { + return plainText.replace(ExamConfigXMLParser.IMPORTED_PASSWORD_MARKER, StringUtils.EMPTY); + } else { + return Utils.hash_SHA_256_Base_16(plainText); + } + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/init/XMLAttributeLoader.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/init/XMLAttributeLoader.java index 27df9302..54980441 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/init/XMLAttributeLoader.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/init/XMLAttributeLoader.java @@ -1,79 +1,87 @@ -/* - * 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.init; - -import java.io.InputStream; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.function.Function; - -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.core.io.ClassPathResource; -import org.springframework.stereotype.Component; - -import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationAttribute; -import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationValue; -import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; -import ch.ethz.seb.sebserver.gbl.util.Utils; -import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.impl.ExamConfigXMLParser; - -@Lazy -@Component -@WebServiceProfile -public class XMLAttributeLoader { - - private static final Logger log = LoggerFactory.getLogger(XMLAttributeLoader.class); - - public Collection loadFromXML( - final Long institutionId, - final Long configurationId, - final Function attributeResolver, - final String xmlFileName) { - - InputStream inputStream; - try { - final ClassPathResource configFileResource = new ClassPathResource(xmlFileName); - inputStream = configFileResource.getInputStream(); - } catch (final Exception e) { - log.error("Failed to get config resources from: {}", xmlFileName, e); - return Collections.emptyList(); - } - - try { - - final Collection values = new ArrayList<>(); - - final ExamConfigXMLParser examConfigImportHandler = new ExamConfigXMLParser( - institutionId, - configurationId, - values::add, - attributeResolver); - - final SAXParserFactory saxParserFactory = SAXParserFactory.newInstance(); - final SAXParser parser = saxParserFactory.newSAXParser(); - parser.parse(inputStream, examConfigImportHandler); - - return Utils.immutableCollectionOf(values); - - } catch (final Exception e) { - log.error("Unexpected error while trying to get initial permitted processes", e); - return Collections.emptyList(); - } finally { - IOUtils.closeQuietly(inputStream); - } - } - -} +/* + * 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.init; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.function.Function; + +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; + +import ch.ethz.seb.sebserver.gbl.util.Cryptor; +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.core.io.ClassPathResource; +import org.springframework.stereotype.Component; + +import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationAttribute; +import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationValue; +import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; +import ch.ethz.seb.sebserver.gbl.util.Utils; +import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.impl.ExamConfigXMLParser; + +@Lazy +@Component +@WebServiceProfile +public class XMLAttributeLoader { + + private static final Logger log = LoggerFactory.getLogger(XMLAttributeLoader.class); + + private final Cryptor cryptor; + + public XMLAttributeLoader(Cryptor cryptor) { + this.cryptor = cryptor; + } + + public Collection loadFromXML( + final Long institutionId, + final Long configurationId, + final Function attributeResolver, + final String xmlFileName) { + + InputStream inputStream; + try { + final ClassPathResource configFileResource = new ClassPathResource(xmlFileName); + inputStream = configFileResource.getInputStream(); + } catch (final Exception e) { + log.error("Failed to get config resources from: {}", xmlFileName, e); + return Collections.emptyList(); + } + + try { + + final Collection values = new ArrayList<>(); + + final ExamConfigXMLParser examConfigImportHandler = new ExamConfigXMLParser( + cryptor, + institutionId, + configurationId, + values::add, + attributeResolver); + + final SAXParserFactory saxParserFactory = SAXParserFactory.newInstance(); + final SAXParser parser = saxParserFactory.newSAXParser(); + parser.parse(inputStream, examConfigImportHandler); + + return Utils.immutableCollectionOf(values); + + } catch (final Exception e) { + log.error("Unexpected error while trying to get initial permitted processes", e); + return Collections.emptyList(); + } finally { + IOUtils.closeQuietly(inputStream); + } + } + +} 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 8b2e86ad..97df2616 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 @@ -1,528 +1,532 @@ -/* - * 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.weblayer.api; - -import java.io.BufferedInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; - -import javax.servlet.ServletOutputStream; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.validation.Valid; - -import org.apache.commons.io.IOUtils; -import org.apache.commons.lang3.StringUtils; -import org.mybatis.dynamic.sql.SqlTable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.util.MultiValueMap; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestBody; -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; -import org.springframework.web.bind.annotation.RestController; - -import ch.ethz.seb.sebserver.gbl.api.API; -import ch.ethz.seb.sebserver.gbl.api.EntityType; -import ch.ethz.seb.sebserver.gbl.api.POSTMapper; -import ch.ethz.seb.sebserver.gbl.api.authorization.PrivilegeType; -import ch.ethz.seb.sebserver.gbl.model.Domain; -import ch.ethz.seb.sebserver.gbl.model.Domain.EXAM; -import ch.ethz.seb.sebserver.gbl.model.EntityKey; -import ch.ethz.seb.sebserver.gbl.model.Page; -import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigCreationInfo; -import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigKey; -import ch.ethz.seb.sebserver.gbl.model.sebconfig.Configuration; -import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode; -import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode.ConfigurationStatus; -import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode.ConfigurationType; -import ch.ethz.seb.sebserver.gbl.model.sebconfig.TemplateAttribute; -import ch.ethz.seb.sebserver.gbl.model.user.UserLogActivityType; -import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; -import ch.ethz.seb.sebserver.gbl.util.Result; -import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ConfigurationNodeRecordDynamicSqlSupport; -import ch.ethz.seb.sebserver.webservice.servicelayer.PaginationService; -import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.AuthorizationService; -import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.UserService; -import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.impl.SEBServerUser; -import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkActionService; -import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationDAO; -import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationNodeDAO; -import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; -import ch.ethz.seb.sebserver.webservice.servicelayer.dao.OrientationDAO; -import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO; -import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ViewDAO; -import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ExamConfigService; -import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ExamConfigTemplateService; -import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationService; - -@WebServiceProfile -@RestController -@RequestMapping("${sebserver.webservice.api.admin.endpoint}" + API.CONFIGURATION_NODE_ENDPOINT) -public class ConfigurationNodeController extends EntityController { - - private static final Logger log = LoggerFactory.getLogger(ConfigurationNodeController.class); - - private final ConfigurationNodeDAO configurationNodeDAO; - private final ConfigurationDAO configurationDAO; - private final ViewDAO viewDAO; - private final OrientationDAO orientationDAO; - private final ExamConfigService sebExamConfigService; - private final ExamConfigTemplateService sebExamConfigTemplateService; - - protected ConfigurationNodeController( - final AuthorizationService authorization, - final BulkActionService bulkActionService, - final ConfigurationNodeDAO entityDAO, - final UserActivityLogDAO userActivityLogDAO, - final PaginationService paginationService, - final BeanValidationService beanValidationService, - final ConfigurationDAO configurationDAO, - final ViewDAO viewDAO, - final OrientationDAO orientationDAO, - final ExamConfigService sebExamConfigService, - final ExamConfigTemplateService sebExamConfigTemplateService) { - - super(authorization, - bulkActionService, - entityDAO, - userActivityLogDAO, - paginationService, - beanValidationService); - - this.configurationDAO = configurationDAO; - this.configurationNodeDAO = entityDAO; - this.viewDAO = viewDAO; - this.orientationDAO = orientationDAO; - this.sebExamConfigService = sebExamConfigService; - this.sebExamConfigTemplateService = sebExamConfigTemplateService; - } - - @Override - protected ConfigurationNode createNew(final POSTMapper postParams) { - final Long institutionId = postParams.getLong(API.PARAM_INSTITUTION_ID); - final SEBServerUser currentUser = this.authorization.getUserService().getCurrentUser(); - postParams.putIfAbsent(EXAM.ATTR_OWNER, currentUser.uuid()); - return new ConfigurationNode(institutionId, postParams); - } - - @Override - protected SqlTable getSQLTableOfEntity() { - return ConfigurationNodeRecordDynamicSqlSupport.configurationNodeRecord; - } - - @RequestMapping( - path = API.MODEL_ID_VAR_PATH_SEGMENT + API.CONFIGURATION_FOLLOWUP_PATH_SEGMENT, - method = RequestMethod.GET, - consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, - produces = MediaType.APPLICATION_JSON_UTF8_VALUE) - public Configuration getFollowup(@PathVariable final Long modelId) { - - this.entityDAO - .byPK(modelId) - .flatMap(this::checkModifyAccess) - .getOrThrow(); - - return this.configurationDAO - .getFollowupConfiguration(modelId) - .getOrThrow(); - } - - @RequestMapping( - path = API.CONFIGURATION_COPY_PATH_SEGMENT, - method = RequestMethod.PUT, - consumes = MediaType.APPLICATION_JSON_UTF8_VALUE, - produces = MediaType.APPLICATION_JSON_UTF8_VALUE) - public ConfigurationNode copyConfiguration( - @RequestParam( - name = API.PARAM_INSTITUTION_ID, - required = true, - defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId, - @Valid @RequestBody final ConfigCreationInfo copyInfo) { - - this.entityDAO.byPK(copyInfo.configurationNodeId) - .flatMap(this.authorization::checkWrite); - - final SEBServerUser currentUser = this.authorization - .getUserService() - .getCurrentUser(); - - return this.configurationNodeDAO.createCopy( - institutionId, - currentUser.getUserInfo().uuid, - copyInfo) - .map(config -> { - if (config.type == ConfigurationType.TEMPLATE) { - return this.createTemplate(config); - } else { - return config; - } - }) - .getOrThrow(); - } - - @RequestMapping( - path = API.MODEL_ID_VAR_PATH_SEGMENT + API.CONFIGURATION_CONFIG_KEY_PATH_SEGMENT, - method = RequestMethod.GET, - consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, - produces = MediaType.APPLICATION_JSON_UTF8_VALUE) - public ConfigKey getConfigKey( - @PathVariable final Long modelId, - @RequestParam( - name = API.PARAM_INSTITUTION_ID, - required = true, - defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId) { - - this.entityDAO.byPK(modelId) - .flatMap(this.authorization::checkRead); - - final String configKey = this.sebExamConfigService - .generateConfigKey(institutionId, modelId) - .getOrThrow(); - - return new ConfigKey(configKey); - } - - @RequestMapping( - path = API.MODEL_ID_VAR_PATH_SEGMENT + API.CONFIGURATION_PLAIN_XML_DOWNLOAD_PATH_SEGMENT, - method = RequestMethod.GET, - produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) - public void downloadPlainXMLConfig( - @PathVariable final Long modelId, - @RequestParam( - name = API.PARAM_INSTITUTION_ID, - required = true, - defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId, - final HttpServletResponse response) throws IOException { - - this.entityDAO.byPK(modelId) - .flatMap(this.authorization::checkRead) - .flatMap(this.userActivityLogDAO::logExport); - - final ServletOutputStream outputStream = response.getOutputStream(); - - try { - this.sebExamConfigService.exportPlainXML( - outputStream, - institutionId, - modelId); - - response.setStatus(HttpStatus.OK.value()); - } catch (final Exception e) { - log.error("Unexpected error while trying to downstream exam config: ", e); - response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); - } finally { - outputStream.flush(); - outputStream.close(); - } - } - - @RequestMapping( - path = API.CONFIGURATION_IMPORT_PATH_SEGMENT, - method = RequestMethod.POST, - consumes = MediaType.APPLICATION_OCTET_STREAM_VALUE, - produces = MediaType.APPLICATION_JSON_UTF8_VALUE) - public Configuration importExamConfig( - @RequestHeader(name = Domain.CONFIGURATION_NODE.ATTR_NAME, required = false) final String name, - @RequestHeader(name = Domain.CONFIGURATION_NODE.ATTR_DESCRIPTION, - required = false) final String description, - @RequestHeader(name = Domain.CONFIGURATION_NODE.ATTR_TEMPLATE_ID, required = false) final String templateId, - @RequestHeader(name = API.IMPORT_PASSWORD_ATTR_NAME, required = false) 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 { - - this.checkModifyPrivilege(institutionId); - - final SEBServerUser currentUser = this.authorization.getUserService().getCurrentUser(); - - final ConfigurationNode configurationNode = new ConfigurationNode( - null, - institutionId, - StringUtils.isNotBlank(templateId) ? Long.parseLong(templateId) : null, - name, - description, - ConfigurationType.EXAM_CONFIG, - currentUser.uuid(), - ConfigurationStatus.CONSTRUCTION); - - final Configuration followup = this.beanValidationService.validateBean(configurationNode) - .flatMap(this.entityDAO::createNew) - .flatMap(this.configurationDAO::getFollowupConfiguration) - .getOrThrow(); - - final Result doImport = doImport(password, request, followup); - if (doImport.hasError()) { - - // rollback of the new configuration - this.configurationNodeDAO.delete(new HashSet<>(Arrays.asList(new EntityKey( - followup.configurationNodeId, - EntityType.CONFIGURATION_NODE)))); - } - - return doImport - .getOrThrow(); - } - - @RequestMapping( - path = API.MODEL_ID_VAR_PATH_SEGMENT + API.CONFIGURATION_IMPORT_PATH_SEGMENT, - method = RequestMethod.POST, - consumes = MediaType.APPLICATION_OCTET_STREAM_VALUE, - produces = MediaType.APPLICATION_JSON_UTF8_VALUE) - public Configuration importExamConfigOnExistingConfig( - @PathVariable final Long modelId, - @RequestHeader(name = API.IMPORT_PASSWORD_ATTR_NAME, required = false) 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 { - - this.entityDAO.byPK(modelId) - .flatMap(this.authorization::checkModify); - - final Configuration newConfig = this.configurationDAO - .saveToHistory(modelId) - .flatMap(this.configurationDAO::restoreToDefaultValues) - .getOrThrow(); - - final Result doImport = doImport(password, request, newConfig); - if (doImport.hasError()) { - - // rollback of the existing values - this.configurationDAO.undo(newConfig.configurationNodeId); - - } - - return doImport - .getOrThrow(); - } - - @RequestMapping( - path = API.PARENT_MODEL_ID_VAR_PATH_SEGMENT + API.TEMPLATE_ATTRIBUTE_ENDPOINT, - method = RequestMethod.GET, - consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, - produces = MediaType.APPLICATION_JSON_UTF8_VALUE) - public Page getTemplateAttributePage( - @PathVariable(name = API.PARAM_PARENT_MODEL_ID, required = true) final Long parentModelId, - @RequestParam( - name = API.PARAM_INSTITUTION_ID, - required = true, - defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId, - @RequestParam(name = Page.ATTR_PAGE_NUMBER, required = false) final Integer pageNumber, - @RequestParam(name = Page.ATTR_PAGE_SIZE, required = false) final Integer pageSize, - @RequestParam(name = Page.ATTR_SORT, required = false) final String sort, - @RequestParam final MultiValueMap allRequestParams) { - - // at least current user must have read access for specified entity type within its own institution - checkReadPrivilege(institutionId); - - final FilterMap filterMap = new FilterMap(allRequestParams); - - // if current user has no read access for specified entity type within other institution - // then the current users institutionId is put as a SQL filter criteria attribute to extends query performance - if (!this.authorization.hasGrant(PrivilegeType.READ, getGrantEntityType())) { - filterMap.putIfAbsent(API.PARAM_INSTITUTION_ID, String.valueOf(institutionId)); - } - - final List attrs = this.sebExamConfigTemplateService - .getTemplateAttributes( - institutionId, - parentModelId, - sort, - filterMap) - .getOrThrow(); - - final int start = (pageNumber - 1) * pageSize; - int end = start + pageSize; - if (attrs.size() < end) { - end = attrs.size(); - } - - return new Page<>( - attrs.size() / pageSize, - pageNumber, - sort, - attrs.subList(start, end)); - } - - @RequestMapping( - path = API.PARENT_MODEL_ID_VAR_PATH_SEGMENT - + API.TEMPLATE_ATTRIBUTE_ENDPOINT - + API.MODEL_ID_VAR_PATH_SEGMENT, - method = RequestMethod.GET, - consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, - produces = MediaType.APPLICATION_JSON_UTF8_VALUE) - public TemplateAttribute getTemplateAttribute( - @PathVariable(name = API.PARAM_PARENT_MODEL_ID, required = true) final Long parentModelId, - @PathVariable(name = API.PARAM_MODEL_ID, required = true) final Long modelId, - @RequestParam( - name = API.PARAM_INSTITUTION_ID, - required = true, - defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId) { - - // at least current user must have read access for specified entity type within its own institution - checkReadPrivilege(institutionId); - return this.sebExamConfigTemplateService - .getAttribute( - institutionId, - parentModelId, - modelId) - .getOrThrow(); - } - - @RequestMapping( - path = API.PARENT_MODEL_ID_VAR_PATH_SEGMENT - + API.TEMPLATE_ATTRIBUTE_ENDPOINT - + API.MODEL_ID_VAR_PATH_SEGMENT - + API.TEMPLATE_ATTRIBUTE_RESET_VALUES, - method = RequestMethod.PATCH, - consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, - produces = MediaType.APPLICATION_JSON_UTF8_VALUE) - public TemplateAttribute resetTemplateAttributeValues( - @PathVariable(name = API.PARAM_PARENT_MODEL_ID, required = true) final Long parentModelId, - @PathVariable(name = API.PARAM_MODEL_ID, required = true) final Long modelId, - @RequestParam( - name = API.PARAM_INSTITUTION_ID, - required = true, - defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId) { - - checkModifyPrivilege(institutionId); - return this.sebExamConfigTemplateService - .setDefaultValues( - institutionId, - parentModelId, - modelId) - .flatMap(entity -> this.userActivityLogDAO.log(UserLogActivityType.MODIFY, entity)) - .getOrThrow(); - } - - @RequestMapping( - path = API.PARENT_MODEL_ID_VAR_PATH_SEGMENT - + API.TEMPLATE_ATTRIBUTE_ENDPOINT - + API.MODEL_ID_VAR_PATH_SEGMENT - + API.TEMPLATE_ATTRIBUTE_ATTACH_DEFAUL_ORIENTATION, - method = RequestMethod.PATCH, - consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, - produces = MediaType.APPLICATION_JSON_UTF8_VALUE) - public TemplateAttribute attachDefaultTemplateAttributeOrientation( - @PathVariable(name = API.PARAM_PARENT_MODEL_ID, required = true) final Long parentModelId, - @PathVariable(name = API.PARAM_MODEL_ID, required = true) final Long modelId, - @RequestParam( - name = API.PARAM_INSTITUTION_ID, - required = true, - defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId, - @RequestParam(name = API.PARAM_VIEW_ID, required = false) final Long viewId) { - - checkModifyPrivilege(institutionId); - - return this.sebExamConfigTemplateService - .attachDefaultOrientation( - institutionId, - parentModelId, - modelId, - viewId) - .flatMap(entity -> this.userActivityLogDAO.log(UserLogActivityType.MODIFY, entity)) - .getOrThrow(); - } - - @RequestMapping( - path = API.PARENT_MODEL_ID_VAR_PATH_SEGMENT - + API.TEMPLATE_ATTRIBUTE_ENDPOINT - + API.MODEL_ID_VAR_PATH_SEGMENT - + API.TEMPLATE_ATTRIBUTE_REMOVE_ORIENTATION, - method = RequestMethod.PATCH, - consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, - produces = MediaType.APPLICATION_JSON_UTF8_VALUE) - public TemplateAttribute removeTemplateAttributeOrientation( - @PathVariable(name = API.PARAM_PARENT_MODEL_ID, required = true) final Long parentModelId, - @PathVariable(name = API.PARAM_MODEL_ID, required = true) final Long modelId, - @RequestParam( - name = API.PARAM_INSTITUTION_ID, - required = true, - defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId) { - - checkModifyPrivilege(institutionId); - return this.sebExamConfigTemplateService - .removeOrientation( - institutionId, - parentModelId, - modelId) - .flatMap(entity -> this.userActivityLogDAO.log(UserLogActivityType.MODIFY, entity)) - .getOrThrow(); - } - - @Override - protected Result validForSave(final ConfigurationNode entity) { - return super.validForSave(entity) - .map(e -> { - final ConfigurationNode existingNode = this.entityDAO.byPK(entity.id) - .getOrThrow(); - if (existingNode.type != entity.type) { - throw new APIConstraintViolationException( - "The Type of ConfigurationNode cannot change after creation"); - } - return e; - }); - } - - @Override - protected Result notifyCreated(final ConfigurationNode entity) { - return super.notifyCreated(entity) - .map(this::createTemplate); - } - - private ConfigurationNode createTemplate(final ConfigurationNode node) { - if (node.type != null && node.type == ConfigurationType.TEMPLATE) { - // create views and orientations for node - return this.viewDAO.copyDefaultViewsForTemplate(node) - .flatMap(viewMapping -> this.orientationDAO.copyDefaultOrientationsForTemplate( - node, - viewMapping)) - .getOrThrow(); - } - return node; - } - - private Result doImport( - final String password, - final HttpServletRequest request, - final Configuration configuration) throws IOException { - final InputStream inputStream = new BufferedInputStream(request.getInputStream()); - try { - - final Configuration result = this.sebExamConfigService.importFromSEBFile( - configuration, - inputStream, - password) - .getOrThrow(); - - return Result.of(result); - - } catch (final Exception e) { - // NOTE: It seems that this has to be manually closed on error case - // We expected that this is closed by the API but if this manual close is been left - // some left-overs will affect strange behavior. - // TODO: find a better solution for this - IOUtils.closeQuietly(inputStream); - return Result.ofError(e); - } - } - -} +/* + * 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.weblayer.api; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; + +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.mybatis.dynamic.sql.SqlTable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.util.MultiValueMap; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +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; +import org.springframework.web.bind.annotation.RestController; + +import ch.ethz.seb.sebserver.gbl.api.API; +import ch.ethz.seb.sebserver.gbl.api.EntityType; +import ch.ethz.seb.sebserver.gbl.api.POSTMapper; +import ch.ethz.seb.sebserver.gbl.api.authorization.PrivilegeType; +import ch.ethz.seb.sebserver.gbl.model.Domain; +import ch.ethz.seb.sebserver.gbl.model.Domain.EXAM; +import ch.ethz.seb.sebserver.gbl.model.EntityKey; +import ch.ethz.seb.sebserver.gbl.model.Page; +import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigCreationInfo; +import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigKey; +import ch.ethz.seb.sebserver.gbl.model.sebconfig.Configuration; +import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode; +import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode.ConfigurationStatus; +import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode.ConfigurationType; +import ch.ethz.seb.sebserver.gbl.model.sebconfig.TemplateAttribute; +import ch.ethz.seb.sebserver.gbl.model.user.UserLogActivityType; +import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; +import ch.ethz.seb.sebserver.gbl.util.Result; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ConfigurationNodeRecordDynamicSqlSupport; +import ch.ethz.seb.sebserver.webservice.servicelayer.PaginationService; +import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.AuthorizationService; +import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.UserService; +import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.impl.SEBServerUser; +import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkActionService; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationDAO; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationNodeDAO; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.OrientationDAO; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ViewDAO; +import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ExamConfigService; +import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ExamConfigTemplateService; +import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationService; + +@WebServiceProfile +@RestController +@RequestMapping("${sebserver.webservice.api.admin.endpoint}" + API.CONFIGURATION_NODE_ENDPOINT) +public class ConfigurationNodeController extends EntityController { + + private static final Logger log = LoggerFactory.getLogger(ConfigurationNodeController.class); + + private final ConfigurationNodeDAO configurationNodeDAO; + private final ConfigurationDAO configurationDAO; + private final ViewDAO viewDAO; + private final OrientationDAO orientationDAO; + private final ExamConfigService sebExamConfigService; + private final ExamConfigTemplateService sebExamConfigTemplateService; + + protected ConfigurationNodeController( + final AuthorizationService authorization, + final BulkActionService bulkActionService, + final ConfigurationNodeDAO entityDAO, + final UserActivityLogDAO userActivityLogDAO, + final PaginationService paginationService, + final BeanValidationService beanValidationService, + final ConfigurationDAO configurationDAO, + final ViewDAO viewDAO, + final OrientationDAO orientationDAO, + final ExamConfigService sebExamConfigService, + final ExamConfigTemplateService sebExamConfigTemplateService) { + + super(authorization, + bulkActionService, + entityDAO, + userActivityLogDAO, + paginationService, + beanValidationService); + + this.configurationDAO = configurationDAO; + this.configurationNodeDAO = entityDAO; + this.viewDAO = viewDAO; + this.orientationDAO = orientationDAO; + this.sebExamConfigService = sebExamConfigService; + this.sebExamConfigTemplateService = sebExamConfigTemplateService; + } + + @Override + protected ConfigurationNode createNew(final POSTMapper postParams) { + final Long institutionId = postParams.getLong(API.PARAM_INSTITUTION_ID); + final SEBServerUser currentUser = this.authorization.getUserService().getCurrentUser(); + postParams.putIfAbsent(EXAM.ATTR_OWNER, currentUser.uuid()); + return new ConfigurationNode(institutionId, postParams); + } + + @Override + protected SqlTable getSQLTableOfEntity() { + return ConfigurationNodeRecordDynamicSqlSupport.configurationNodeRecord; + } + + @RequestMapping( + path = API.MODEL_ID_VAR_PATH_SEGMENT + API.CONFIGURATION_FOLLOWUP_PATH_SEGMENT, + method = RequestMethod.GET, + consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, + produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public Configuration getFollowup(@PathVariable final Long modelId) { + + this.entityDAO + .byPK(modelId) + .flatMap(this::checkModifyAccess) + .getOrThrow(); + + return this.configurationDAO + .getFollowupConfiguration(modelId) + .getOrThrow(); + } + + @RequestMapping( + path = API.CONFIGURATION_COPY_PATH_SEGMENT, + method = RequestMethod.PUT, + consumes = MediaType.APPLICATION_JSON_UTF8_VALUE, + produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public ConfigurationNode copyConfiguration( + @RequestParam( + name = API.PARAM_INSTITUTION_ID, + required = true, + defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId, + @Valid @RequestBody final ConfigCreationInfo copyInfo) { + + this.entityDAO.byPK(copyInfo.configurationNodeId) + .flatMap(this.authorization::checkWrite); + + final SEBServerUser currentUser = this.authorization + .getUserService() + .getCurrentUser(); + + return this.configurationNodeDAO.createCopy( + institutionId, + currentUser.getUserInfo().uuid, + copyInfo) + .map(config -> { + if (config.type == ConfigurationType.TEMPLATE) { + return this.createTemplate(config); + } else { + return config; + } + }) + .getOrThrow(); + } + + @RequestMapping( + path = API.MODEL_ID_VAR_PATH_SEGMENT + API.CONFIGURATION_CONFIG_KEY_PATH_SEGMENT, + method = RequestMethod.GET, + consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, + produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public ConfigKey getConfigKey( + @PathVariable final Long modelId, + @RequestParam( + name = API.PARAM_INSTITUTION_ID, + required = true, + defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId) { + + this.entityDAO.byPK(modelId) + .flatMap(this.authorization::checkRead); + + final String configKey = this.sebExamConfigService + .generateConfigKey(institutionId, modelId) + .getOrThrow(); + + return new ConfigKey(configKey); + } + + @RequestMapping( + path = API.MODEL_ID_VAR_PATH_SEGMENT + API.CONFIGURATION_PLAIN_XML_DOWNLOAD_PATH_SEGMENT, + method = RequestMethod.GET, + produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) + public void downloadPlainXMLConfig( + @PathVariable final Long modelId, + @RequestParam( + name = API.PARAM_INSTITUTION_ID, + required = true, + defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId, + final HttpServletResponse response) throws IOException { + + this.entityDAO.byPK(modelId) + .flatMap(this.authorization::checkRead) + .flatMap(this.userActivityLogDAO::logExport); + + final ServletOutputStream outputStream = response.getOutputStream(); + + try { + this.sebExamConfigService.exportPlainXML( + outputStream, + institutionId, + modelId); + + response.setStatus(HttpStatus.OK.value()); + } catch (final Exception e) { + log.error("Unexpected error while trying to downstream exam config: ", e); + response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); + } finally { + outputStream.flush(); + outputStream.close(); + } + } + + @RequestMapping( + path = API.CONFIGURATION_IMPORT_PATH_SEGMENT, + method = RequestMethod.POST, + consumes = MediaType.APPLICATION_OCTET_STREAM_VALUE, + produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public Configuration importExamConfig( + @RequestHeader(name = Domain.CONFIGURATION_NODE.ATTR_NAME, required = false) final String name, + @RequestHeader(name = Domain.CONFIGURATION_NODE.ATTR_DESCRIPTION, + required = false) final String description, + @RequestHeader(name = Domain.CONFIGURATION_NODE.ATTR_TEMPLATE_ID, required = false) final String templateId, + @RequestHeader(name = API.IMPORT_PASSWORD_ATTR_NAME, required = false) 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 { + + this.checkModifyPrivilege(institutionId); + + final SEBServerUser currentUser = this.authorization.getUserService().getCurrentUser(); + + final ConfigurationNode configurationNode = new ConfigurationNode( + null, + institutionId, + StringUtils.isNotBlank(templateId) ? Long.parseLong(templateId) : null, + name, + description, + ConfigurationType.EXAM_CONFIG, + currentUser.uuid(), + ConfigurationStatus.CONSTRUCTION); + + final Configuration followup = this.beanValidationService.validateBean(configurationNode) + .flatMap(this.entityDAO::createNew) + .flatMap(this.configurationDAO::getFollowupConfiguration) + .getOrThrow(); + + final Result doImport = doImport(password, request, followup); + if (doImport.hasError()) { + + // rollback if the new configuration + this.configurationNodeDAO.delete(new HashSet<>(Arrays.asList(new EntityKey( + followup.configurationNodeId, + EntityType.CONFIGURATION_NODE)))); + } + + Configuration config = doImport + .getOrThrow(); + + return this.configurationDAO + .saveToHistory(config.configurationNodeId) + .getOrThrow(); + } + + @RequestMapping( + path = API.MODEL_ID_VAR_PATH_SEGMENT + API.CONFIGURATION_IMPORT_PATH_SEGMENT, + method = RequestMethod.POST, + consumes = MediaType.APPLICATION_OCTET_STREAM_VALUE, + produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public Configuration importExamConfigOnExistingConfig( + @PathVariable final Long modelId, + @RequestHeader(name = API.IMPORT_PASSWORD_ATTR_NAME, required = false) 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 { + + this.entityDAO.byPK(modelId) + .flatMap(this.authorization::checkModify); + + final Configuration newConfig = this.configurationDAO + .saveToHistory(modelId) + .flatMap(this.configurationDAO::restoreToDefaultValues) + .getOrThrow(); + + final Result doImport = doImport(password, request, newConfig); + if (doImport.hasError()) { + + // rollback of the existing values + this.configurationDAO.undo(newConfig.configurationNodeId); + + } + + return doImport + .getOrThrow(); + } + + @RequestMapping( + path = API.PARENT_MODEL_ID_VAR_PATH_SEGMENT + API.TEMPLATE_ATTRIBUTE_ENDPOINT, + method = RequestMethod.GET, + consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, + produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public Page getTemplateAttributePage( + @PathVariable(name = API.PARAM_PARENT_MODEL_ID, required = true) final Long parentModelId, + @RequestParam( + name = API.PARAM_INSTITUTION_ID, + required = true, + defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId, + @RequestParam(name = Page.ATTR_PAGE_NUMBER, required = false) final Integer pageNumber, + @RequestParam(name = Page.ATTR_PAGE_SIZE, required = false) final Integer pageSize, + @RequestParam(name = Page.ATTR_SORT, required = false) final String sort, + @RequestParam final MultiValueMap allRequestParams) { + + // at least current user must have read access for specified entity type within its own institution + checkReadPrivilege(institutionId); + + final FilterMap filterMap = new FilterMap(allRequestParams); + + // if current user has no read access for specified entity type within other institution + // then the current users institutionId is put as a SQL filter criteria attribute to extends query performance + if (!this.authorization.hasGrant(PrivilegeType.READ, getGrantEntityType())) { + filterMap.putIfAbsent(API.PARAM_INSTITUTION_ID, String.valueOf(institutionId)); + } + + final List attrs = this.sebExamConfigTemplateService + .getTemplateAttributes( + institutionId, + parentModelId, + sort, + filterMap) + .getOrThrow(); + + final int start = (pageNumber - 1) * pageSize; + int end = start + pageSize; + if (attrs.size() < end) { + end = attrs.size(); + } + + return new Page<>( + attrs.size() / pageSize, + pageNumber, + sort, + attrs.subList(start, end)); + } + + @RequestMapping( + path = API.PARENT_MODEL_ID_VAR_PATH_SEGMENT + + API.TEMPLATE_ATTRIBUTE_ENDPOINT + + API.MODEL_ID_VAR_PATH_SEGMENT, + method = RequestMethod.GET, + consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, + produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public TemplateAttribute getTemplateAttribute( + @PathVariable(name = API.PARAM_PARENT_MODEL_ID, required = true) final Long parentModelId, + @PathVariable(name = API.PARAM_MODEL_ID, required = true) final Long modelId, + @RequestParam( + name = API.PARAM_INSTITUTION_ID, + required = true, + defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId) { + + // at least current user must have read access for specified entity type within its own institution + checkReadPrivilege(institutionId); + return this.sebExamConfigTemplateService + .getAttribute( + institutionId, + parentModelId, + modelId) + .getOrThrow(); + } + + @RequestMapping( + path = API.PARENT_MODEL_ID_VAR_PATH_SEGMENT + + API.TEMPLATE_ATTRIBUTE_ENDPOINT + + API.MODEL_ID_VAR_PATH_SEGMENT + + API.TEMPLATE_ATTRIBUTE_RESET_VALUES, + method = RequestMethod.PATCH, + consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, + produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public TemplateAttribute resetTemplateAttributeValues( + @PathVariable(name = API.PARAM_PARENT_MODEL_ID, required = true) final Long parentModelId, + @PathVariable(name = API.PARAM_MODEL_ID, required = true) final Long modelId, + @RequestParam( + name = API.PARAM_INSTITUTION_ID, + required = true, + defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId) { + + checkModifyPrivilege(institutionId); + return this.sebExamConfigTemplateService + .setDefaultValues( + institutionId, + parentModelId, + modelId) + .flatMap(entity -> this.userActivityLogDAO.log(UserLogActivityType.MODIFY, entity)) + .getOrThrow(); + } + + @RequestMapping( + path = API.PARENT_MODEL_ID_VAR_PATH_SEGMENT + + API.TEMPLATE_ATTRIBUTE_ENDPOINT + + API.MODEL_ID_VAR_PATH_SEGMENT + + API.TEMPLATE_ATTRIBUTE_ATTACH_DEFAUL_ORIENTATION, + method = RequestMethod.PATCH, + consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, + produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public TemplateAttribute attachDefaultTemplateAttributeOrientation( + @PathVariable(name = API.PARAM_PARENT_MODEL_ID, required = true) final Long parentModelId, + @PathVariable(name = API.PARAM_MODEL_ID, required = true) final Long modelId, + @RequestParam( + name = API.PARAM_INSTITUTION_ID, + required = true, + defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId, + @RequestParam(name = API.PARAM_VIEW_ID, required = false) final Long viewId) { + + checkModifyPrivilege(institutionId); + + return this.sebExamConfigTemplateService + .attachDefaultOrientation( + institutionId, + parentModelId, + modelId, + viewId) + .flatMap(entity -> this.userActivityLogDAO.log(UserLogActivityType.MODIFY, entity)) + .getOrThrow(); + } + + @RequestMapping( + path = API.PARENT_MODEL_ID_VAR_PATH_SEGMENT + + API.TEMPLATE_ATTRIBUTE_ENDPOINT + + API.MODEL_ID_VAR_PATH_SEGMENT + + API.TEMPLATE_ATTRIBUTE_REMOVE_ORIENTATION, + method = RequestMethod.PATCH, + consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, + produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public TemplateAttribute removeTemplateAttributeOrientation( + @PathVariable(name = API.PARAM_PARENT_MODEL_ID, required = true) final Long parentModelId, + @PathVariable(name = API.PARAM_MODEL_ID, required = true) final Long modelId, + @RequestParam( + name = API.PARAM_INSTITUTION_ID, + required = true, + defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId) { + + checkModifyPrivilege(institutionId); + return this.sebExamConfigTemplateService + .removeOrientation( + institutionId, + parentModelId, + modelId) + .flatMap(entity -> this.userActivityLogDAO.log(UserLogActivityType.MODIFY, entity)) + .getOrThrow(); + } + + @Override + protected Result validForSave(final ConfigurationNode entity) { + return super.validForSave(entity) + .map(e -> { + final ConfigurationNode existingNode = this.entityDAO.byPK(entity.id) + .getOrThrow(); + if (existingNode.type != entity.type) { + throw new APIConstraintViolationException( + "The Type of ConfigurationNode cannot change after creation"); + } + return e; + }); + } + + @Override + protected Result notifyCreated(final ConfigurationNode entity) { + return super.notifyCreated(entity) + .map(this::createTemplate); + } + + private ConfigurationNode createTemplate(final ConfigurationNode node) { + if (node.type != null && node.type == ConfigurationType.TEMPLATE) { + // create views and orientations for node + return this.viewDAO.copyDefaultViewsForTemplate(node) + .flatMap(viewMapping -> this.orientationDAO.copyDefaultOrientationsForTemplate( + node, + viewMapping)) + .getOrThrow(); + } + return node; + } + + private Result doImport( + final String password, + final HttpServletRequest request, + final Configuration configuration) throws IOException { + final InputStream inputStream = new BufferedInputStream(request.getInputStream()); + try { + + final Configuration result = this.sebExamConfigService.importFromSEBFile( + configuration, + inputStream, + password) + .getOrThrow(); + + return Result.of(result); + + } catch (final Exception e) { + // NOTE: It seems that this has to be manually closed on error case + // We expected that this is closed by the API but if this manual close is been left + // some left-overs will affect strange behavior. + // TODO: find a better solution for this + IOUtils.closeQuietly(inputStream); + return Result.ofError(e); + } + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/SebClientConfigController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/SebClientConfigController.java index 617be284..7f9e52cf 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/SebClientConfigController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/SebClientConfigController.java @@ -1,166 +1,215 @@ -/* - * 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.weblayer.api; - -import java.io.IOException; -import java.io.PipedInputStream; -import java.io.PipedOutputStream; - -import javax.servlet.ServletOutputStream; -import javax.servlet.http.HttpServletResponse; - -import org.apache.commons.io.IOUtils; -import org.joda.time.DateTime; -import org.joda.time.DateTimeZone; -import org.mybatis.dynamic.sql.SqlTable; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.scheduling.annotation.EnableAsync; -import org.springframework.validation.FieldError; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RestController; - -import ch.ethz.seb.sebserver.gbl.Constants; -import ch.ethz.seb.sebserver.gbl.api.API; -import ch.ethz.seb.sebserver.gbl.api.APIMessage; -import ch.ethz.seb.sebserver.gbl.api.APIMessage.APIMessageException; -import ch.ethz.seb.sebserver.gbl.api.POSTMapper; -import ch.ethz.seb.sebserver.gbl.model.Domain; -import ch.ethz.seb.sebserver.gbl.model.sebconfig.SebClientConfig; -import ch.ethz.seb.sebserver.gbl.model.user.PasswordChange; -import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; -import ch.ethz.seb.sebserver.gbl.util.Result; -import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.SebClientConfigRecordDynamicSqlSupport; -import ch.ethz.seb.sebserver.webservice.servicelayer.PaginationService; -import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.AuthorizationService; -import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkActionService; -import ch.ethz.seb.sebserver.webservice.servicelayer.dao.SebClientConfigDAO; -import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO; -import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ClientConfigService; -import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationService; - -@WebServiceProfile -@RestController -@EnableAsync -@RequestMapping("${sebserver.webservice.api.admin.endpoint}" + API.SEB_CLIENT_CONFIG_ENDPOINT) -public class SebClientConfigController extends ActivatableEntityController { - - private final ClientConfigService sebClientConfigService; - - public SebClientConfigController( - final SebClientConfigDAO sebClientConfigDAO, - final AuthorizationService authorization, - final UserActivityLogDAO userActivityLogDAO, - final BulkActionService bulkActionService, - final PaginationService paginationService, - final BeanValidationService beanValidationService, - final ClientConfigService sebClientConfigService) { - - super(authorization, - bulkActionService, - sebClientConfigDAO, - userActivityLogDAO, - paginationService, - beanValidationService); - - this.sebClientConfigService = sebClientConfigService; - } - - @RequestMapping( - path = API.SEB_CLIENT_CONFIG_DOWNLOAD_PATH_SEGMENT + API.MODEL_ID_VAR_PATH_SEGMENT, - method = RequestMethod.GET, - produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) - public void downloadSEBConfig( - @PathVariable final String modelId, - final HttpServletResponse response) throws IOException { - - this.entityDAO.byModelId(modelId) - .flatMap(this.authorization::checkWrite) - .map(this.userActivityLogDAO::logExport); - - final ServletOutputStream outputStream = response.getOutputStream(); - PipedOutputStream pout = null; - PipedInputStream pin = null; - try { - pout = new PipedOutputStream(); - pin = new PipedInputStream(pout); - - this.sebClientConfigService.exportSebClientConfiguration( - pout, - modelId); - - IOUtils.copyLarge(pin, outputStream); - - response.setStatus(HttpStatus.OK.value()); - - outputStream.flush(); - - } finally { - outputStream.flush(); - outputStream.close(); - } - -// final StreamingResponseBody stream = out -> { -// this.sebClientConfigService.exportSebClientConfiguration( -// out, -// modelId); -// }; -// -// return new ResponseEntity<>(stream, HttpStatus.OK); - } - - @Override - protected SebClientConfig createNew(final POSTMapper postParams) { - - final Long institutionId = postParams.getLong( - Domain.SEB_CLIENT_CONFIGURATION.ATTR_INSTITUTION_ID); - - if (institutionId == null) { - throw new APIConstraintViolationException("Institution identifier is missing"); - } - - postParams.putIfAbsent( - Domain.SEB_CLIENT_CONFIGURATION.ATTR_DATE, - DateTime.now(DateTimeZone.UTC).toString(Constants.DEFAULT_DATE_TIME_FORMAT)); - - return new SebClientConfig(institutionId, postParams); - } - - @Override - protected SqlTable getSQLTableOfEntity() { - return SebClientConfigRecordDynamicSqlSupport.sebClientConfigRecord; - } - - @Override - protected Result validForCreate(final SebClientConfig entity) { - return super.validForCreate(entity) - .map(this::checkPasswordMatch); - } - - @Override - protected Result validForSave(final SebClientConfig entity) { - return super.validForSave(entity) - .map(this::checkPasswordMatch); - } - - private SebClientConfig checkPasswordMatch(final SebClientConfig entity) { - if (entity.hasEncryptionSecret() && !entity.encryptSecret.equals(entity.confirmEncryptSecret)) { - throw new APIMessageException(APIMessage.fieldValidationError( - new FieldError( - Domain.SEB_CLIENT_CONFIGURATION.TYPE_NAME, - PasswordChange.ATTR_NAME_PASSWORD, - "clientConfig:confirm_encrypt_secret:password.mismatch"))); - } - - return entity; - } - -} +/* + * 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.weblayer.api; + +import java.io.IOException; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.util.ArrayList; +import java.util.Collection; + +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.StringUtils; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.mybatis.dynamic.sql.SqlTable; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +import ch.ethz.seb.sebserver.gbl.Constants; +import ch.ethz.seb.sebserver.gbl.api.API; +import ch.ethz.seb.sebserver.gbl.api.APIMessage; +import ch.ethz.seb.sebserver.gbl.api.APIMessage.APIMessageException; +import ch.ethz.seb.sebserver.gbl.api.POSTMapper; +import ch.ethz.seb.sebserver.gbl.model.Domain; +import ch.ethz.seb.sebserver.gbl.model.sebconfig.SebClientConfig; +import ch.ethz.seb.sebserver.gbl.model.user.PasswordChange; +import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; +import ch.ethz.seb.sebserver.gbl.util.Result; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.SebClientConfigRecordDynamicSqlSupport; +import ch.ethz.seb.sebserver.webservice.servicelayer.PaginationService; +import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.AuthorizationService; +import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkActionService; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.SebClientConfigDAO; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO; +import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ClientConfigService; +import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationService; + +@WebServiceProfile +@RestController +@EnableAsync +@RequestMapping("${sebserver.webservice.api.admin.endpoint}" + API.SEB_CLIENT_CONFIG_ENDPOINT) +public class SebClientConfigController extends ActivatableEntityController { + + private final ClientConfigService sebClientConfigService; + + public SebClientConfigController( + final SebClientConfigDAO sebClientConfigDAO, + final AuthorizationService authorization, + final UserActivityLogDAO userActivityLogDAO, + final BulkActionService bulkActionService, + final PaginationService paginationService, + final BeanValidationService beanValidationService, + final ClientConfigService sebClientConfigService) { + + super(authorization, + bulkActionService, + sebClientConfigDAO, + userActivityLogDAO, + paginationService, + beanValidationService); + + this.sebClientConfigService = sebClientConfigService; + } + + @RequestMapping( + path = API.SEB_CLIENT_CONFIG_DOWNLOAD_PATH_SEGMENT + API.MODEL_ID_VAR_PATH_SEGMENT, + method = RequestMethod.GET, + produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) + public void downloadSEBConfig( + @PathVariable final String modelId, + final HttpServletResponse response) throws IOException { + + this.entityDAO.byModelId(modelId) + .flatMap(this.authorization::checkWrite) + .map(this.userActivityLogDAO::logExport); + + final ServletOutputStream outputStream = response.getOutputStream(); + PipedOutputStream pout = null; + PipedInputStream pin = null; + try { + pout = new PipedOutputStream(); + pin = new PipedInputStream(pout); + + this.sebClientConfigService.exportSebClientConfiguration( + pout, + modelId); + + IOUtils.copyLarge(pin, outputStream); + + response.setStatus(HttpStatus.OK.value()); + + outputStream.flush(); + + } finally { + outputStream.flush(); + outputStream.close(); + } + } + + @Override + protected SebClientConfig createNew(final POSTMapper postParams) { + + final Long institutionId = postParams.getLong( + Domain.SEB_CLIENT_CONFIGURATION.ATTR_INSTITUTION_ID); + + if (institutionId == null) { + throw new APIConstraintViolationException("Institution identifier is missing"); + } + + postParams.putIfAbsent( + Domain.SEB_CLIENT_CONFIGURATION.ATTR_DATE, + DateTime.now(DateTimeZone.UTC).toString(Constants.DEFAULT_DATE_TIME_FORMAT)); + + return new SebClientConfig(institutionId, postParams); + } + + @Override + protected SqlTable getSQLTableOfEntity() { + return SebClientConfigRecordDynamicSqlSupport.sebClientConfigRecord; + } + + @Override + protected Result validForCreate(final SebClientConfig entity) { + return super.validForCreate(entity) + .map(this::checkPasswordMatch); + } + + @Override + protected Result validForSave(final SebClientConfig entity) { + return super.validForSave(entity) + .map(this::checkPasswordMatch); + } + + private SebClientConfig checkPasswordMatch(final SebClientConfig entity) { + Collection errors = new ArrayList<>(); + if (entity.hasEncryptionSecret() && !entity.encryptSecret.equals(entity.encryptSecretConfirm)) { + errors.add(APIMessage.fieldValidationError( + new FieldError( + Domain.SEB_CLIENT_CONFIGURATION.TYPE_NAME, + PasswordChange.ATTR_NAME_PASSWORD, + "clientConfig:confirm_encrypt_secret:password.mismatch"))); + } + + if (entity.hasFallbackPassword() && !entity.fallbackPassword.equals(entity.fallbackPasswordConfirm)) { + errors.add(APIMessage.fieldValidationError( + new FieldError( + Domain.SEB_CLIENT_CONFIGURATION.TYPE_NAME, + SebClientConfig.ATTR_FALLBACK_PASSWORD_CONFIRM, + "clientConfig:sebServerFallbackPasswordHashConfirm:password.mismatch"))); + } + + if (entity.hasQuitPassword() && !entity.quitPassword.equals(entity.quitPasswordConfirm)) { + errors.add(APIMessage.fieldValidationError( + new FieldError( + Domain.SEB_CLIENT_CONFIGURATION.TYPE_NAME, + SebClientConfig.ATTR_QUIT_PASSWORD_CONFIRM, + "clientConfig:hashedQuitPasswordConfirm:password.mismatch"))); + } + + if (BooleanUtils.isTrue(entity.fallback) && StringUtils.isBlank(entity.fallbackStartURL)) { + errors.add(APIMessage.fieldValidationError( + new FieldError( + Domain.SEB_CLIENT_CONFIGURATION.TYPE_NAME, + SebClientConfig.ATTR_FALLBACK_START_URL, + "clientConfig:startURL:notNull"))); + } + + if (BooleanUtils.isTrue(entity.fallback) && entity.fallbackTimeout == null) { + errors.add(APIMessage.fieldValidationError( + new FieldError( + Domain.SEB_CLIENT_CONFIGURATION.TYPE_NAME, + SebClientConfig.ATTR_FALLBACK_TIMEOUT, + "clientConfig:sebServerFallbackTimeout:notNull"))); + } + + if (BooleanUtils.isTrue(entity.fallback) && entity.fallbackAttempts == null) { + errors.add(APIMessage.fieldValidationError( + new FieldError( + Domain.SEB_CLIENT_CONFIGURATION.TYPE_NAME, + SebClientConfig.ATTR_FALLBACK_ATTEMPTS, + "clientConfig:sebServerFallbackAttempts:notNull"))); + } + + if (BooleanUtils.isTrue(entity.fallback) && entity.fallbackAttemptInterval == null) { + errors.add(APIMessage.fieldValidationError( + new FieldError( + Domain.SEB_CLIENT_CONFIGURATION.TYPE_NAME, + SebClientConfig.ATTR_FALLBACK_ATTEMPT_INTERVAL, + "clientConfig:sebServerFallbackAttemptInterval:notNull"))); + } + + if (!errors.isEmpty()) { + throw new APIMessage.APIMessageException(errors); + } + + return entity; + } + +} diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index ba78cba0..62094802 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -23,6 +23,8 @@ sebserver.overall.action.goAwayFromEditPageConfirm=Are you sure you want to leav sebserver.overall.action.category.varia= sebserver.overall.action.category.filter= +sebserver.overall.action.showPassword.tooltip=Show / hide password in plain text. + sebserver.overall.status.active=Active sebserver.overall.status.inactive=Inactive sebserver.overall.status.all=All @@ -567,7 +569,7 @@ sebserver.exam.indicator.thresholds.list.add=Add a new threshold sebserver.exam.indicator.thresholds.list.remove=Delete this threshold ################################ -# SEB Client Configuration +# SEB client configuration ################################ sebserver.sebconfig.activity.name=SEB Configuration @@ -576,7 +578,7 @@ sebserver.clientconfig.action.list=Client Configuration sebserver.clientconfig.action.export=Export sebserver.clientconfig.list.empty=There is currently no SEB-Client configuration available. Please create a new one -sebserver.clientconfig.list.title=SEB Client Configurations +sebserver.clientconfig.list.title=SEB client configurations sebserver.clientconfig.list.actions= sebserver.clientconfig.list.column.institution=Institution sebserver.clientconfig.list.column.institution.tooltip=The institution of the SEB client configuration.

Use the filter above to specify the institution.
{0} @@ -587,24 +589,31 @@ sebserver.clientconfig.list.column.date.tooltip=The date when the SEB client con sebserver.clientconfig.list.column.active=Active sebserver.clientconfig.list.column.active.tooltip=The activity of SEB client configuration.

Use the filter above to specify the activity.
{0} sebserver.clientconfig.info.pleaseSelect=Please select first a Client Configuration from the list -sebserver.clientconfig.list.action.no.modify.privilege=No Access: A SEB Client Configuration from other institution cannot be modified. +sebserver.clientconfig.list.action.no.modify.privilege=No Access: A SEB client configuration from other institution cannot be modified. sebserver.clientconfig.form.title.new=Add Client Configuration -sebserver.clientconfig.form.title=SEB Client Configuration +sebserver.clientconfig.form.title=SEB client configuration sebserver.clientconfig.form.name=Name -sebserver.clientconfig.form.name.tooltip=The name of the SEB Client Configuration.
Can be any name that not already exists for another SEB Client Configuration +sebserver.clientconfig.form.name.tooltip=The name of the SEB client configuration.
Can be any name that not already exists for another SEB client configuration sebserver.clientconfig.form.fallback=With Fallback -sebserver.clientconfig.form.fallback.tooltip=Indicates whether this SEB Client Configuration has a fallback definition or not +sebserver.clientconfig.form.fallback.tooltip=Indicates whether this SEB client configuration has a fallback definition or not sebserver.clientconfig.form.fallback-url=Fallback Start URL sebserver.clientconfig.form.fallback-url.tooltip=A fallback URL that tells the SEB where to go when the SEB Server service is unavailable. sebserver.clientconfig.form.sebServerFallbackTimeout=Fallback Timeout -sebserver.clientconfig.form.sebServerFallbackTimeout.tooltip=Defines the fallback timeout for the SEB Client in milli-seconds. -sebserver.clientconfig.form.sebServerFallbackAttempts=Fallback Attempts -sebserver.clientconfig.form.sebServerFallbackAttempts.tooltip=The number of connection attempts a SEB Client is trying before switching to fallback case. -sebserver.clientconfig.form.sebServerFallbackAttemptInterval=Attempt Interval -sebserver.clientconfig.form.sebServerFallbackAttemptInterval.tooltip=The interval (in milli-seconds) between connection attempts a SEB Client shall use. +sebserver.clientconfig.form.sebServerFallbackTimeout.tooltip=Defines the fallback timeout for the SEB client in milli-seconds. +sebserver.clientconfig.form.sebServerFallbackAttempts=Connection Attempts +sebserver.clientconfig.form.sebServerFallbackAttempts.tooltip=The number of connection attempts a SEB client is trying before switching to fallback case. +sebserver.clientconfig.form.sebServerFallbackAttemptInterval=Interval +sebserver.clientconfig.form.sebServerFallbackAttemptInterval.tooltip=The interval (in milli-seconds) between connection attempts a SEB client shall use. sebserver.clientconfig.form.sebServerFallbackPasswordHash=Fallback Password -sebserver.clientconfig.form.sebServerFallbackPasswordHash.tooltip=A password if set, a SEB Client user must give before the SEB Client starts the fallback procedure. +sebserver.clientconfig.form.sebServerFallbackPasswordHash.tooltip=A password if set a SEB Client user must provide before the SEB client starts the fallback procedure. +sebserver.clientconfig.form.sebServerFallbackPasswordHash.confirm=Confirm Fallback Password +sebserver.clientconfig.form.sebServerFallbackPasswordHash.tooltip.confirm=Please confirm the fallback password +sebserver.clientconfig.form.hashedQuitPassword=Quit Password +sebserver.clientconfig.form.hashedQuitPassword.tooltip=A password if set a SEB client user must provide to be able to quit the SEB client. +sebserver.clientconfig.form.hashedQuitPassword.confirm=Confirm Quit Password +sebserver.clientconfig.form.hashedQuitPassword.tooltip.confirm=Please confirm the quit password + sebserver.clientconfig.form.date=Creation Date sebserver.clientconfig.form.date.tooltip=The date when the SEB client configuration was first created. sebserver.clientconfig.form.encryptSecret=Configuration Password @@ -614,6 +623,11 @@ sebserver.clientconfig.form.encryptSecret.confirm.tooltip=Please retype the give sebserver.clientconfig.form.sebConfigPurpose=Configuration Purpose sebserver.clientconfig.form.sebConfigPurpose.tooltip=This indicates whether this client configuration shall be used to configure the SEB Client or to start an exam +sebserver.clientconfig.config.purpose.START_EXAM=Starting an Exam +sebserver.clientconfig.config.purpose.START_EXAM.tooltip=If the SEB client configuration is loaded via a SEB-Link, the local configuration will not be overwritten. +sebserver.clientconfig.config.purpose.CONFIGURE_CLIENT=Configure a Client +sebserver.clientconfig.config.purpose.CONFIGURE_CLIENT.tooltip=If the SEB client configuration is loaded via a SEB-Link, the local configuration will be overwritten by this configuration. + sebserver.clientconfig.action.list.new=Add Configuration sebserver.clientconfig.action.list.view=View Configuration sebserver.clientconfig.action.list.modify=Edit Configuration diff --git a/src/main/resources/static/css/sebserver.css b/src/main/resources/static/css/sebserver.css index 63430a45..729c8f55 100644 --- a/src/main/resources/static/css/sebserver.css +++ b/src/main/resources/static/css/sebserver.css @@ -1,921 +1,941 @@ -* { - font: 12px Arial, Helvetica, sans-serif; - color: #4a4a4a; - background-image: none; - background-color: #FFFFFF; - padding: 0; -} - -*:disabled { - color: #CFCFCF; -} - -/* Label default theme */ -Label { - font: 12px Arial, Helvetica, sans-serif; - color: #4a4a4a; - background-color: transparent; - background-image: none; - background-repeat: repeat; - background-position: left top; - border: none; - border-radius: 0; - text-decoration: none; - cursor: default; - opacity: 1; - text-shadow: none; - padding: 0px 0px 0px 0px; -} - -Label.head { - font: bold 12px Arial, Helvetica, sans-serif; - padding: 0px 0px 0px 0px; -} - -Label.form-center { - padding: 5px 0px 0px 0px; -} - -Label.action { - font: 12px Arial, Helvetica, sans-serif; - color: #82BE1E; - background-color: transparent; - background-image: none; - background-repeat: repeat; - background-position: left top; - border: none; - border-radius: 0; - text-decoration: none; - cursor: default; - opacity: 1; - text-shadow: none; -} - -Label.list-nav { - font: 12px Arial, Helvetica, sans-serif; - color: #ffffff; - background-color: #595959; - background-image: none; - background-repeat: repeat; - background-position: left top; - border: none; - border-radius: 0; - text-decoration: none; - cursor: default; - opacity: 1; - text-shadow: none; -} - -Label.h1 { - font: 25px Arial, Helvetica, sans-serif; - height: 28px; - padding: 0px 12px 6px 12px; - color: #1f407a; -} - -Label.h2 { - font: 19px Arial, Helvetica, sans-serif; - height: 22px; - padding: 0 0 6px 0; - color: #1f407a; -} - -Label.h3 { - font: bold 14px Arial, Helvetica, sans-serif; - height: 20px; - padding: 0; - color: #1f407a; -} - -Label.error { - font: 10px Arial, Helvetica, sans-serif; - color: #aa0000; -} - -Label:hover.imageButton { - background-color: transparent; - background-repeat: no-repeat; -} - -Label.selection { - padding: 4px 6px 3px 6px; -} - -Label:hover.selection { - color: #4a4a4a; - background-color: #b5b5b5; - background-image: gradient(linear, left top, left bottom, from(#b5b5b5),to(#b5b5b5)); - padding: 4px 6px 3px 6px; -} - -Label.selected { - color: #4a4a4a; - background-color: #c5c5c5; - background-image: gradient(linear, left top, left bottom, from(#c5c5c5),to(#c5c5c5)); - padding: 4px 6px 3px 6px; -} - -Label-SeparatorLine { - background-image: none; - background-color: transparent; - border: 1px solid #bdbdbd; - border-radius: 0px; - height: 1px; -} - -Label.colordark { - font: 12px "Courier New", Courier, monospace; - color: #4a4a4a; - padding: 2px 5px 2px 5px; -} - -Label.colorlight { - font: 12px "Courier New", Courier, monospace; - color: #FFFFFF; - padding: 2px 5px 2px 5px; -} - -Composite { - background-color: transparent; -} - - - -Composite.bordered { - border: 2px; -} - -Composite.header { - background-color: #000000; - color: #FFFFFF; -} - -Composite.logo { - background-color: #1F407A; -} - -Composite.bgLogo { - background-color: #1F407A; - background-image: url(static/images/ethz_logo_white.png); - background-repeat: no-repeat; - background-position: left center; -} - -Composite.bgLogoNoImage { - background-color: transparent; - background-repeat: no-repeat; - background-position: left center; -} - -Composite.bgContent { - background-color: #EAECEE; - background-image: url(static/images/blueBackground.png); - background-repeat: repeat-x; -} - -Composite.content { - background-color: #FFFFFF; - margin: 0 0 0 0; -} - -Composite.actionPane { - background-color: #D3D9DB; -} - -Composite.bgFooter { - background-color: #EAECEE; -} - -Composite.footer { - background-color: #1F407A; -} - -Composite.login { - background-color: #EAECEE; - margin: 20px 0 0 0; - padding: 15px 8px 8px 8px; - border: 1px solid #bdbdbd; - border-radius: 2px; -} - -Composite.register { - background-color: #EAECEE; - margin: 20px 0 0 0; - padding: 15px 8px 8px 8px; - border: none; -} - -Composite.login-back { - background-color: #EAECEE; - margin: 0px 0 0 0; - padding: 0px 0px 0px 0px; -} - -Composite.error { - border: 1px solid #aa0000; - border-radius: 1px; -} - -Composite.warning { - background-gradient-color: rgba( 168, 50, 45, 0.5 ); - background-image: gradient( linear, left top, left bottom, from(rgba( 168, 50, 45, 0.5 ) ), to( rgba( 168, 50, 45, 0.5 ) ) ); - background-repeat: repeat; - background-position: left top; - opacity: 1; -} - -*.header { - font: bold 12px Arial, Helvetica, sans-serif; - color: #FFFFFF; - background-color: transparent; -} - -*.footer { - font: bold 12px Arial, Helvetica, sans-serif; - color: #FFFFFF; - background-color: transparent; -} - -/* Group default theme */ -Group { - font: 10px Arial, Helvetica, sans-serif; - color: #4a4a4a; - background-color: #ffffff; - border: none; -} - -Group-Frame { - margin: 10px 0 0 0; - padding: 10px 0px 0px 0px; - border: 1px solid #bdbdbd; - border-radius: 2px; -} - -Group-Label { - padding: 2px 10px 2px 10px; - background-color: #ffffff; - background-image: none; - background-repeat: repeat; - background-position: left top; - border: 0px solid #bdbdbd; - border-radius: 0px; - color: inherit; - margin: 0px 0px 0px 10px; - text-shadow: none; -} - - -/* Text default */ -Text { - font: 12px Arial, Helvetica, sans-serif; - border: none; - border-radius: 0; - padding: 3px 10px 3px 10px; - color: #4a4a4a; - background-repeat: repeat; - background-position: left top; - background-color: #ffffff; - background-image: none; - text-shadow: none; - box-shadow: none; -} - -Text.error { - border: 1px solid #aa0000; -} - -Text[BORDER], Text[MULTI][BORDER] { - border: 1px solid #aaaaaa; - border-radius: 0; - box-shadow: none; -} - -Text[BORDER].error, Text[MULTI][BORDER].error { - border: 1px solid #aa0000; - border-radius: 0; - box-shadow: none; -} - -Text[BORDER]:focused, Text[MULTI][BORDER]:focused { - border: 1px solid #4f7cb1; - border-radius: 0; - box-shadow: none; -} - - -Text[BORDER]:disabled.inputreadonly, -Text[BORDER]:read-only.inputreadonly, -Text[MULTI][BORDER]:disabled.inputreadonly, -Text[MULTI][BORDER]:read-only.inputreadonly { - font: 12px Arial, Helvetica, sans-serif; - border: 1px solid #aaaaaa; - border-radius: 0; - padding: 0px 10px 0px 10px; - color: #aaaaaa; - background-repeat: repeat; - background-position: left top; - background-color: #ffffff; - background-image: none; - text-shadow: none; - box-shadow: none; -} - -Text:disabled, -Text:read-only, -Text[MULTI]:disabled, -Text[MULTI]:read-only { - box-shadow: none; - background-color: #ffffff; - border: none; - border-radius: 0; - color: #4a4a4a; - padding: 0px 0px 0px 0px; -} - -Text:disabled.colorbox, -Text:read-only.colorbox, -Text[MULTI]:disabled.colorbox, -Text[MULTI]:read-only.colorbox { - box-shadow: none; - border: none; - border-radius: 0; - padding: 0px 10px 0px 10px; -} - - -/* Combo default theme */ -Combo, Combo[BORDER] { - font: 12px Arial, Helvetica, sans-serif; - color: #4a4a4a; - background-color: #ffffff; - border: 1px solid #aaaaaa; - border-radius: 0 2px 2px 0; - background-image: none; - text-shadow: none; - box-shadow: none; -} - -Combo:focused, Combo[BORDER]:focused { - text-shadow: none; - box-shadow: none; -} - -Combo:disabled, Combo[BORDER]:disabled { - text-shadow: none; - box-shadow: none; -} - -Combo-Button { - cursor: default; - background-color: #ffffff; - background-image: gradient(linear, left top, left bottom, from(#ffffff), to(#ffffff)); - border: none; - width: 20px; -} - -Combo-Field { - padding: 3px 0px 1px 10px; -} - -Combo.error, , Combo[BORDER].error { - border: 1px solid #aa0000; - border-radius: 0 2px 2px 0; -} - -/* DateTime default theme */ -DateTime, DateTime[BORDER] { - font: 12px Arial, Helvetica, sans-serif; - color: #4a4a4a; - background-color: #ffffff; - border: 1px solid #aaaaaa; - border-radius: 0 2px 2px 0; - background-image: none; - text-shadow: none; - box-shadow: none; -} - -DateTime, DateTime[BORDER]:focused { - text-shadow: none; - box-shadow: none; -} - -DateTime-Field, DateTime-Field[BORDER] { - font: 12px Arial, Helvetica, sans-serif; - color: #4a4a4a; - background-color: #ffffff; - padding: 3px 10px 2px 10px; - text-shadow: none; - box-shadow: none; -} - -DateTime-UpButton { - cursor: default; - background-color: #ffffff; - background-image: gradient(linear, left top, left bottom, from(#ffffff), to(#ffffff)); - border: none; - width: 30px; -} - -DateTime-DownButton { - cursor: default; - background-color: #ffffff; - background-image: gradient(linear, left top, left bottom, from(#ffffff), to(#ffffff)); - border: none; - width: 30px; -} - -DateTime-DropDownButton { - cursor: default; - background-color: #ffffff; - background-image: gradient(linear, left top, left bottom, from(#ffffff), to(#ffffff)); - border: none; - width: 30px; -} - -DateTime-Calendar-Navbar { - border: none; - border-radius: 0; - background-color: #1F407A; - background-image: gradient(linear, left top, left bottom, from(#1F407A), to(#1F407A)); - color: white; - font: 12px Arial, Helvetica, sans-serif; - text-shadow: none; -} - -DateTime-Field:selected, DateTime-Calendar-Day:selected { - background-color: #1F407A; - color: #ffffff; -} - -DateTime-Calendar-Day:selected:hover { - background-color: #1F407A; - color: #ffffff; -} - -/* Message titlebar */ -Shell.message { - font: 12px Arial, Helvetica, sans-serif; - animation: none; - border: 1px solid #bdbdbd; - background-color: #ffffff; - background-image: none; - opacity: 1; - box-shadow: none; - padding: 5px 10px 5px 10px; -} - -Shell-Titlebar.message { - background-color: #1f407a; - background-gradient-color: #1f407a; - color: white; - background-image: gradient( linear, left top, left bottom, from( #1f407a ), to( #1f407a ) ); - padding: 2px 5px 2px; - margin: 0px; - height: 22px; - font: 14px Arial, Helvetica, sans-serif; - border: none; - border-radius: 1px 1px 0px 0px; - text-shadow: none; -} - -Shell-CloseButton:hover.message { - background-color: #82BE1E; - background-gradient-color: #82BE1E; - background-image: gradient( linear, left top, left bottom, from( #82BE1E ), to( #82BE1E ) ); -} - -Button { - font: 12px Arial, Helvetica, sans-serif; - padding: 5px 6px 5px 6px; -} - -/* Push Buttons */ -Button[PUSH], -Button[PUSH]:default { - font: bold 12px Arial, Helvetica, sans-serif; - background-color: #0069B4; - background-gradient-color: #0069B4; - background-image: gradient( linear, left top, left bottom, from( #0069B4 ), to( #0069B4 ) ); - color: #fff; - border: none; - border-radius: 0px; - padding: 6px 15px; - text-shadow: none; -} - -Button[PUSH]:pressed { - background-color: #444; - color: #fff; - background-gradient-color: #444; - background-image: gradient( linear, left top, left bottom, from( #444 ), to( #444 ) ); -} - -Button[PUSH]:hover { - background-color: #82BE1E; - background-gradient-color: #82BE1E; - background-image: gradient( linear, left top, left bottom, from( #82BE1E ), to( #82BE1E ) ); - color: #444; - cursor: pointer; -} - -Button[PUSH]:disabled { - background-color: transparent; - border: 1px solid #EAECEE; - color: #c0c0c0; - background-repeat: no-repeat; - background-position: right; -} - -Button-FocusIndicator[PUSH][BORDER] { - background-color: transparent; -} - -/* Push Buttons header */ -Button[PUSH].header, -Button[PUSH]:default.header { - font: bold 12px Arial, Helvetica, sans-serif; - background-color: #595959; - background-gradient-color: #595959; - background-image: gradient( linear, left top, left bottom, from( #595959 ), to( #595959 ) ); - color: #fff; - border: none; - border-radius: 0px; - padding: 6px 15px; - text-shadow: none; -} - -Button[PUSH]:pressed.header { - background-color: #444; - color: #fff; - background-gradient-color: #444; - background-image: gradient( linear, left top, left bottom, from( #444 ), to( #444 ) ); -} - -Button[PUSH]:hover.header { - background-color: #82BE1E; - background-gradient-color: #82BE1E; - background-image: gradient( linear, left top, left bottom, from( #82BE1E ), to( #82BE1E ) ); - color: #444; - cursor: pointer; -} - -Button[CHECK]:disabled { - color: #4a4a4a; -} - -Button[RADIO]:disabled { - color: #4a4a4a; -} - -FileUpload, -FileUpload:default, -FileUpload:hover, -FileUpload:pressed { - background-color: transparent; - background-gradient-color: transparent; - background-image: gradient( linear, left top, left bottom, from( transparent ), to( transparent ) ); - border: none; - border-radius: 0px; - text-shadow: none; - margin: 0px 0px 0px 0px; - padding: 0px 0px 0px 0px; -} - - -/* Sash default */ -Sash { - background-image: none; - background-color: transparent; - background-color: #EAECEE; -} - -Sash:hover { - background-color: #444444; -} - - -/*Standard Einstellungen fuer Trees*/ -Tree { - font: bold 14px Arial, Helvetica, sans-serif; - background-color: transparent; - border: none; - color: #1f407a; - margin: 0px 0px 0px 0px; - padding: 0px 0px 0px 40px; -} - -Tree[BORDER] { - border: 1px solid #eceeef; -} - -TreeItem, TreeItem.treesection, Tree-RowOverlay:hover.treesection, Tree-RowOverlay:selected.treesection, Tree-RowOverlay:selected:hover.treesection { - font: bold 14px Arial, Helvetica, sans-serif; - color: #1f407a; - background-color: transparent; - text-decoration: none; - text-shadow: none; - background-image: none; - margin: 20px 20px 20px 20px; - padding: 20px 20px 20px 40px; -} - -TreeItem:linesvisible:even { - background-color: #f3f3f4; -} - - -Tree-RowOverlay { - background-color: transparent; - color: inherit; - background-image: none; -} - -Tree-RowOverlay:hover { - background-color: #82be1e; - color: #1F407A; -} - - -Tree-RowOverlay:selected { - background-color: #82be1e; - color: #1F407A; -} - -Tree-RowOverlay:selected:unfocused { - background-color: #82be1e; - color: #1f407a; -} - -Tree-RowOverlay:selected:hover { - background-color: #82be1e; - color: #000000; -} - -Tree.actions { - font: 12px Arial, Helvetica, sans-serif; - color: #4a4a4a; - background-color: transparent; - border: none; - margin: 0 0 0 0; -} - -Tree[BORDER].actions { - border: 1px solid #eceeef; -} - -TreeItem.actions { - font: 12px Arial, Helvetica, sans-serif; - color: #4a4a4a; - background-color: transparent; - text-decoration: none; - text-shadow: none; - background-image: none; - margin: 0 0 0 0; -} - - -Tree-RowOverlay:hover.actions { - background-color: #82be1e; - color: #4a4a4a; -} - -Tree-RowOverlay:selected.actions { - background-color: #595959; - color: #4a4a4a; -} - - - -/* TabFolder default theme */ - -TabFolder { - font: 12px Arial, Helvetica, sans-serif; - color: #4a4a4a; - border: none; -} - -TabFolder-ContentContainer { - border: none; - border-top: 1px solid #bdbdbd; -} - -TabItem { - font: 12px Arial, Helvetica, sans-serif; - color: #4a4a4a; - background-color: #FFFFFF; - text-decoration: none; - text-shadow: none; - background-image: none; - margin: 1px 0px 0px 0px; - border: 1px solid #bdbdbd; - border-bottom: none; - border-left: none; -} - -TabItem:selected { - background-color: #D3D9DB; - background-gradient-color: #D3D9DB; - background-image: gradient( linear, left top, left bottom, from( #D3D9DB ), to( #D3D9DB ) ); - color: #4a4a4a; - margin: 0px 0px 0px 0px; - border: 1px solid #bdbdbd; - border-bottom: none; - border-left: none; -} - -TabItem:hover { - background-color: #82be1e; - background-gradient-color: #82be1e; - background-image: gradient( linear, left top, left bottom, from( #82be1e ), to( #82be1e ) ); - color: #4a4a4a; - margin: 1px 0px 0px -1px; - border-left: none; -} - -TabItem:selected:hover { - background-color: #82be1e; - background-gradient-color: #82be1e; - background-image: gradient( linear, left top, left bottom, from( #82be1e ), to( #82be1e ) ); - color: #4a4a4a; - margin: 0px 0px 0px 0px; - border-left: none; -} - -TabItem:first { - font: 12px Arial, Helvetica, sans-serif; - color: #4a4a4a; - background-color: #FFFFFF; - text-decoration: none; - text-shadow: none; - background-image: none; - margin: 1px 0px 0px -1px; - border: 1px solid #bdbdbd; - border-bottom: none; -} - -TabItem:first:hover { - background-color: #82be1e; - background-gradient-color: #82be1e; - background-image: gradient( linear, left top, left bottom, from( #82be1e ), to( #82be1e ) ); - color: #4a4a4a; - margin: 1px 0px 0px -1px; - border-left: none; -} - -TabItem:selected:first { - background-color: #D3D9DB; - color: #4a4a4a; - margin: 0px 0px 0px 0px; - border-left: 1px solid #bdbdbd; -} - -TabItem:selected:hover:first { - background-color: #82be1e; - background-gradient-color: #82be1e; - background-image: gradient( linear, left top, left bottom, from( #82be1e ), to( #82be1e ) ); - color: #4a4a4a; - margin: 0px 0px 0px 0px; - border-left: 1px solid #bdbdbd; -} - - -Widget-ToolTip { - padding: 10px 10px 10px 10px; - background-color: #D3D9DB; - border: 1px solid #3C5A0F; - border-radius: 1px 1px 1px 1px; - color: #4a4a4a; - opacity: 1; - animation: fadeIn 200ms linear, fadeOut 600ms ease-out; - box-shadow: 3px 4px 2px rgba(0, 0, 0, 0.3); - text-align: left; -} - -Widget-ToolTip-Pointer { - background-image: none; -} - -/* Table default theme */ -Table { - font: 12px Arial, Helvetica, sans-serif; - background-color: #ffffff; - background-image: none; - color: #4a4a4a; - border: none; -} - -Table[BORDER] { - border: 1px solid #bdbdbd; -} - -TableColumn { - font: 12px Arial, Helvetica, sans-serif; - background-color: #595959; - background-gradient-color: #595959; - background-image: gradient( linear, left top, left bottom, from( #595959 ), to( #595959 ) ); - padding: 4px 3px 4px 3px; - - color: #FFFFFF; - border-bottom: 1px solid #bdbdbd; - text-shadow: none; -} - -TableColumn:hover { - background-color: #595959; - background-gradient-color: #595959; - background-image: gradient( linear, left top, left bottom, from( #595959 ), to( #595959 ) ); -} - -TableItem { - background-color: transparent; - color: inherit; - text-decoration: none; - text-shadow: none; - background-image: none; -} - -Table-RowOverlay.warning { - background-color: rgba( 168, 50, 45, 0.5 ); - background-gradient-color: rgba( 168, 50, 45, 0.5 ); - background-image: gradient( linear, left top, left bottom, from(rgba( 168, 50, 45, 0.5 ) ), to( rgba( 168, 50, 45, 0.5 ) ) ); -} - -TableItem:linesvisible:even { - background-color: #ffffff; - color: inherit; -} - -Table-RowOverlay { - background-color: transparent; - color: inherit; - background-image: none; -} - - -Table-RowOverlay:hover { - color: #4a4a4a; - background-color: #b5b5b5; - background-image: gradient(linear, left top, left bottom, from(#b5b5b5), to(#b5b5b5)); -} - -Table-RowOverlay:selected { - color: #4a4a4a; - background-color: #c5c5c5; - background-image: gradient(linear, left top, left bottom, from(#c5c5c5),to(#c5c5c5)); -} - -Table-RowOverlay:selected:unfocused { - color: #4a4a4a; - background-color: #c5c5c5; - background-image: gradient(linear, left top, left bottom, from(#c5c5c5),to(#c5c5c5)); -} - -Table-RowOverlay:linesvisible:even:hover { - color: #4a4a4a; - background-color: #b5b5b5; - background-image: gradient(linear, left top, left bottom, from(#b5b5b5),to(#b5b5b5)); -} - -Table-RowOverlay:linesvisible:even:selected { - color: #4a4a4a; - background-color: #c5c5c5; - background-image: gradient(linear, left top, left bottom, from(#c5c5c5),to(#c5c5c5)); -} - -Table-RowOverlay:linesvisible:even:selected:unfocused { - background-color: #c5c5c5; - background-image: gradient(linear, left top, left bottom, from(#c5c5c5),to(#c5c5c5)); - color: #4a4a4a; -} - -TableColumn-SortIndicator { - background-image: none; -} -/* -TableColumn-SortIndicator:up { - background-image: url( themes/images/column/sort-indicator-up.png ); -} - -TableColumn-SortIndicator:down { - background-image: url( themes/images/column/sort-indicator-down.png ); -} -*/ - -Table-Cell { - spacing: 3px; - padding: 5px 3px 5px 3px; -} - -Table-GridLine, Table-GridLine:vertical:rowtemplate { - color: transparent; -} - -Table-GridLine:vertical, Table-GridLine:header, Table-GridLine:horizontal:rowtemplate { - color: transparent; -} - - - - - - +* { + font: 12px Arial, Helvetica, sans-serif; + color: #4a4a4a; + background-image: none; + background-color: #FFFFFF; + padding: 0; +} + +*:disabled { + color: #CFCFCF; +} + +/* Label default theme */ +Label { + font: 12px Arial, Helvetica, sans-serif; + color: #4a4a4a; + background-color: transparent; + background-image: none; + background-repeat: repeat; + background-position: left top; + border: none; + border-radius: 0; + text-decoration: none; + cursor: default; + opacity: 1; + text-shadow: none; + padding: 0px 0px 0px 0px; +} + +Label.head { + font: bold 12px Arial, Helvetica, sans-serif; + padding: 0px 0px 0px 0px; +} + +Label.form-center { + padding: 5px 0px 0px 0px; +} + +Label.action { + font: 12px Arial, Helvetica, sans-serif; + color: #82BE1E; + background-color: transparent; + background-image: none; + background-repeat: repeat; + background-position: left top; + border: none; + border-radius: 0; + text-decoration: none; + cursor: default; + opacity: 1; + text-shadow: none; +} + +Label.list-nav { + font: 12px Arial, Helvetica, sans-serif; + color: #ffffff; + background-color: #595959; + background-image: none; + background-repeat: repeat; + background-position: left top; + border: none; + border-radius: 0; + text-decoration: none; + cursor: default; + opacity: 1; + text-shadow: none; +} + +Label.h1 { + font: 25px Arial, Helvetica, sans-serif; + height: 28px; + padding: 0px 12px 6px 12px; + color: #1f407a; +} + +Label.h2 { + font: 19px Arial, Helvetica, sans-serif; + height: 22px; + padding: 0 0 6px 0; + color: #1f407a; +} + +Label.h3 { + font: bold 14px Arial, Helvetica, sans-serif; + height: 20px; + padding: 0; + color: #1f407a; +} + +Label.error { + font: 10px Arial, Helvetica, sans-serif; + color: #aa0000; +} + +Label:hover.imageButton { + background-color: transparent; + background-repeat: no-repeat; +} + +Label.selection { + padding: 4px 6px 3px 6px; +} + +Label:hover.selection { + color: #4a4a4a; + background-color: #b5b5b5; + background-image: gradient(linear, left top, left bottom, from(#b5b5b5),to(#b5b5b5)); + padding: 4px 6px 3px 6px; +} + +Label.selected { + color: #4a4a4a; + background-color: #c5c5c5; + background-image: gradient(linear, left top, left bottom, from(#c5c5c5),to(#c5c5c5)); + padding: 4px 6px 3px 6px; +} + +Label-SeparatorLine { + background-image: none; + background-color: transparent; + border: 1px solid #bdbdbd; + border-radius: 0px; + height: 1px; +} + +Label.colordark { + font: 12px "Courier New", Courier, monospace; + color: #4a4a4a; + padding: 2px 5px 2px 5px; +} + +Label.colorlight { + font: 12px "Courier New", Courier, monospace; + color: #FFFFFF; + padding: 2px 5px 2px 5px; +} + +Composite { + background-color: transparent; +} + + + +Composite.bordered { + border: 2px; +} + +Composite.header { + background-color: #000000; + color: #FFFFFF; +} + +Composite.logo { + background-color: #1F407A; +} + +Composite.bgLogo { + background-color: #1F407A; + background-image: url(static/images/ethz_logo_white.png); + background-repeat: no-repeat; + background-position: left center; +} + +Composite.bgLogoNoImage { + background-color: transparent; + background-repeat: no-repeat; + background-position: left center; +} + +Composite.bgContent { + background-color: #EAECEE; + background-image: url(static/images/blueBackground.png); + background-repeat: repeat-x; +} + +Composite.content { + background-color: #FFFFFF; + margin: 0 0 0 0; +} + +Composite.actionPane { + background-color: #D3D9DB; +} + +Composite.bgFooter { + background-color: #EAECEE; +} + +Composite.footer { + background-color: #1F407A; +} + +Composite.login { + background-color: #EAECEE; + margin: 20px 0 0 0; + padding: 15px 8px 8px 8px; + border: 1px solid #bdbdbd; + border-radius: 2px; +} + +Composite.register { + background-color: #EAECEE; + margin: 20px 0 0 0; + padding: 15px 8px 8px 8px; + border: none; +} + +Composite.login-back { + background-color: #EAECEE; + margin: 0px 0 0 0; + padding: 0px 0px 0px 0px; +} + +Composite.error { + border: 1px solid #aa0000; + border-radius: 1px; +} + +Composite.warning { + background-gradient-color: rgba( 168, 50, 45, 0.5 ); + background-image: gradient( linear, left top, left bottom, from(rgba( 168, 50, 45, 0.5 ) ), to( rgba( 168, 50, 45, 0.5 ) ) ); + background-repeat: repeat; + background-position: left top; + opacity: 1; +} + +*.header { + font: bold 12px Arial, Helvetica, sans-serif; + color: #FFFFFF; + background-color: transparent; +} + +*.footer { + font: bold 12px Arial, Helvetica, sans-serif; + color: #FFFFFF; + background-color: transparent; +} + +/* Group default theme */ +Group { + font: 10px Arial, Helvetica, sans-serif; + color: #4a4a4a; + background-color: #ffffff; + border: none; +} + +Group-Frame { + margin: 10px 0 0 0; + padding: 10px 0px 0px 0px; + border: 1px solid #bdbdbd; + border-radius: 2px; +} + +Group-Label { + padding: 2px 10px 2px 10px; + background-color: #ffffff; + background-image: none; + background-repeat: repeat; + background-position: left top; + border: 0px solid #bdbdbd; + border-radius: 0px; + color: inherit; + margin: 0px 0px 0px 10px; + text-shadow: none; +} + + +/* Text default */ +Text { + font: 12px Arial, Helvetica, sans-serif; + border: none; + border-radius: 0; + padding: 3px 10px 3px 10px; + color: #4a4a4a; + background-repeat: repeat; + background-position: left top; + background-color: #ffffff; + background-image: none; + text-shadow: none; + box-shadow: none; +} + +Text.error { + border: 1px solid #aa0000; +} + +Text[BORDER], Text[MULTI][BORDER] { + border: 1px solid #aaaaaa; + border-radius: 0; + box-shadow: none; +} + +Text[BORDER].error, Text[MULTI][BORDER].error { + border: 1px solid #aa0000; + border-radius: 0; + box-shadow: none; +} + +Text[BORDER]:focused, Text[MULTI][BORDER]:focused { + border: 1px solid #4f7cb1; + border-radius: 0; + box-shadow: none; +} + + +Text[BORDER]:disabled.inputreadonly, +Text[BORDER]:read-only.inputreadonly, +Text[MULTI][BORDER]:disabled.inputreadonly, +Text[MULTI][BORDER]:read-only.inputreadonly { + font: 12px Arial, Helvetica, sans-serif; + border: 1px solid #aaaaaa; + border-radius: 0; + padding: 0px 10px 0px 10px; + color: #aaaaaa; + background-repeat: repeat; + background-position: left top; + background-color: #ffffff; + background-image: none; + text-shadow: none; + box-shadow: none; +} + +Text.pwdplain, +Text:disabled.pwdplain, +Text:read-only.pwdplain, +Text[BORDER]:disabled.pwdplain, +Text[BORDER]:read-only.pwdplain { + font: 12px Arial, Helvetica, sans-serif; + border: 1px solid #aaaaaa; + border-radius: 0; + padding: 3px 10px 3px 10px; + color: #aaaaaa; + background-repeat: repeat; + background-position: left top; + background-color: #ffffff; + background-image: none; + text-shadow: none; + box-shadow: none; +} + + + +Text:disabled, +Text:read-only, +Text[MULTI]:disabled, +Text[MULTI]:read-only { + box-shadow: none; + background-color: #ffffff; + border: none; + border-radius: 0; + color: #4a4a4a; + padding: 0px 0px 0px 0px; +} + +Text:disabled.colorbox, +Text:read-only.colorbox, +Text[MULTI]:disabled.colorbox, +Text[MULTI]:read-only.colorbox { + box-shadow: none; + border: none; + border-radius: 0; + padding: 0px 10px 0px 10px; +} + + +/* Combo default theme */ +Combo, Combo[BORDER] { + font: 12px Arial, Helvetica, sans-serif; + color: #4a4a4a; + background-color: #ffffff; + border: 1px solid #aaaaaa; + border-radius: 0 2px 2px 0; + background-image: none; + text-shadow: none; + box-shadow: none; +} + +Combo:focused, Combo[BORDER]:focused { + text-shadow: none; + box-shadow: none; +} + +Combo:disabled, Combo[BORDER]:disabled { + text-shadow: none; + box-shadow: none; +} + +Combo-Button { + cursor: default; + background-color: #ffffff; + background-image: gradient(linear, left top, left bottom, from(#ffffff), to(#ffffff)); + border: none; + width: 20px; +} + +Combo-Field { + padding: 3px 0px 1px 10px; +} + +Combo.error, , Combo[BORDER].error { + border: 1px solid #aa0000; + border-radius: 0 2px 2px 0; +} + +/* DateTime default theme */ +DateTime, DateTime[BORDER] { + font: 12px Arial, Helvetica, sans-serif; + color: #4a4a4a; + background-color: #ffffff; + border: 1px solid #aaaaaa; + border-radius: 0 2px 2px 0; + background-image: none; + text-shadow: none; + box-shadow: none; +} + +DateTime, DateTime[BORDER]:focused { + text-shadow: none; + box-shadow: none; +} + +DateTime-Field, DateTime-Field[BORDER] { + font: 12px Arial, Helvetica, sans-serif; + color: #4a4a4a; + background-color: #ffffff; + padding: 3px 10px 2px 10px; + text-shadow: none; + box-shadow: none; +} + +DateTime-UpButton { + cursor: default; + background-color: #ffffff; + background-image: gradient(linear, left top, left bottom, from(#ffffff), to(#ffffff)); + border: none; + width: 30px; +} + +DateTime-DownButton { + cursor: default; + background-color: #ffffff; + background-image: gradient(linear, left top, left bottom, from(#ffffff), to(#ffffff)); + border: none; + width: 30px; +} + +DateTime-DropDownButton { + cursor: default; + background-color: #ffffff; + background-image: gradient(linear, left top, left bottom, from(#ffffff), to(#ffffff)); + border: none; + width: 30px; +} + +DateTime-Calendar-Navbar { + border: none; + border-radius: 0; + background-color: #1F407A; + background-image: gradient(linear, left top, left bottom, from(#1F407A), to(#1F407A)); + color: white; + font: 12px Arial, Helvetica, sans-serif; + text-shadow: none; +} + +DateTime-Field:selected, DateTime-Calendar-Day:selected { + background-color: #1F407A; + color: #ffffff; +} + +DateTime-Calendar-Day:selected:hover { + background-color: #1F407A; + color: #ffffff; +} + +/* Message titlebar */ +Shell.message { + font: 12px Arial, Helvetica, sans-serif; + animation: none; + border: 1px solid #bdbdbd; + background-color: #ffffff; + background-image: none; + opacity: 1; + box-shadow: none; + padding: 5px 10px 5px 10px; +} + +Shell-Titlebar.message { + background-color: #1f407a; + background-gradient-color: #1f407a; + color: white; + background-image: gradient( linear, left top, left bottom, from( #1f407a ), to( #1f407a ) ); + padding: 2px 5px 2px; + margin: 0px; + height: 22px; + font: 14px Arial, Helvetica, sans-serif; + border: none; + border-radius: 1px 1px 0px 0px; + text-shadow: none; +} + +Shell-CloseButton:hover.message { + background-color: #82BE1E; + background-gradient-color: #82BE1E; + background-image: gradient( linear, left top, left bottom, from( #82BE1E ), to( #82BE1E ) ); +} + +Button { + font: 12px Arial, Helvetica, sans-serif; + padding: 5px 6px 5px 6px; +} + +/* Push Buttons */ +Button[PUSH], +Button[PUSH]:default { + font: bold 12px Arial, Helvetica, sans-serif; + background-color: #0069B4; + background-gradient-color: #0069B4; + background-image: gradient( linear, left top, left bottom, from( #0069B4 ), to( #0069B4 ) ); + color: #fff; + border: none; + border-radius: 0px; + padding: 6px 15px; + text-shadow: none; +} + +Button[PUSH]:pressed { + background-color: #444; + color: #fff; + background-gradient-color: #444; + background-image: gradient( linear, left top, left bottom, from( #444 ), to( #444 ) ); +} + +Button[PUSH]:hover { + background-color: #82BE1E; + background-gradient-color: #82BE1E; + background-image: gradient( linear, left top, left bottom, from( #82BE1E ), to( #82BE1E ) ); + color: #444; + cursor: pointer; +} + +Button[PUSH]:disabled { + background-color: transparent; + border: 1px solid #EAECEE; + color: #c0c0c0; + background-repeat: no-repeat; + background-position: right; +} + +Button-FocusIndicator[PUSH][BORDER] { + background-color: transparent; +} + +/* Push Buttons header */ +Button[PUSH].header, +Button[PUSH]:default.header { + font: bold 12px Arial, Helvetica, sans-serif; + background-color: #595959; + background-gradient-color: #595959; + background-image: gradient( linear, left top, left bottom, from( #595959 ), to( #595959 ) ); + color: #fff; + border: none; + border-radius: 0px; + padding: 6px 15px; + text-shadow: none; +} + +Button[PUSH]:pressed.header { + background-color: #444; + color: #fff; + background-gradient-color: #444; + background-image: gradient( linear, left top, left bottom, from( #444 ), to( #444 ) ); +} + +Button[PUSH]:hover.header { + background-color: #82BE1E; + background-gradient-color: #82BE1E; + background-image: gradient( linear, left top, left bottom, from( #82BE1E ), to( #82BE1E ) ); + color: #444; + cursor: pointer; +} + +Button[CHECK]:disabled { + color: #4a4a4a; +} + +Button[RADIO]:disabled { + color: #4a4a4a; +} + +FileUpload, +FileUpload:default, +FileUpload:hover, +FileUpload:pressed { + background-color: transparent; + background-gradient-color: transparent; + background-image: gradient( linear, left top, left bottom, from( transparent ), to( transparent ) ); + border: none; + border-radius: 0px; + text-shadow: none; + margin: 0px 0px 0px 0px; + padding: 0px 0px 0px 0px; +} + + +/* Sash default */ +Sash { + background-image: none; + background-color: transparent; + background-color: #EAECEE; +} + +Sash:hover { + background-color: #444444; +} + + +/*Standard Einstellungen fuer Trees*/ +Tree { + font: bold 14px Arial, Helvetica, sans-serif; + background-color: transparent; + border: none; + color: #1f407a; + margin: 0px 0px 0px 0px; + padding: 0px 0px 0px 40px; +} + +Tree[BORDER] { + border: 1px solid #eceeef; +} + +TreeItem, TreeItem.treesection, Tree-RowOverlay:hover.treesection, Tree-RowOverlay:selected.treesection, Tree-RowOverlay:selected:hover.treesection { + font: bold 14px Arial, Helvetica, sans-serif; + color: #1f407a; + background-color: transparent; + text-decoration: none; + text-shadow: none; + background-image: none; + margin: 20px 20px 20px 20px; + padding: 20px 20px 20px 40px; +} + +TreeItem:linesvisible:even { + background-color: #f3f3f4; +} + + +Tree-RowOverlay { + background-color: transparent; + color: inherit; + background-image: none; +} + +Tree-RowOverlay:hover { + background-color: #82be1e; + color: #1F407A; +} + + +Tree-RowOverlay:selected { + background-color: #82be1e; + color: #1F407A; +} + +Tree-RowOverlay:selected:unfocused { + background-color: #82be1e; + color: #1f407a; +} + +Tree-RowOverlay:selected:hover { + background-color: #82be1e; + color: #000000; +} + +Tree.actions { + font: 12px Arial, Helvetica, sans-serif; + color: #4a4a4a; + background-color: transparent; + border: none; + margin: 0 0 0 0; +} + +Tree[BORDER].actions { + border: 1px solid #eceeef; +} + +TreeItem.actions { + font: 12px Arial, Helvetica, sans-serif; + color: #4a4a4a; + background-color: transparent; + text-decoration: none; + text-shadow: none; + background-image: none; + margin: 0 0 0 0; +} + + +Tree-RowOverlay:hover.actions { + background-color: #82be1e; + color: #4a4a4a; +} + +Tree-RowOverlay:selected.actions { + background-color: #595959; + color: #4a4a4a; +} + + + +/* TabFolder default theme */ + +TabFolder { + font: 12px Arial, Helvetica, sans-serif; + color: #4a4a4a; + border: none; +} + +TabFolder-ContentContainer { + border: none; + border-top: 1px solid #bdbdbd; +} + +TabItem { + font: 12px Arial, Helvetica, sans-serif; + color: #4a4a4a; + background-color: #FFFFFF; + text-decoration: none; + text-shadow: none; + background-image: none; + margin: 1px 0px 0px 0px; + border: 1px solid #bdbdbd; + border-bottom: none; + border-left: none; +} + +TabItem:selected { + background-color: #D3D9DB; + background-gradient-color: #D3D9DB; + background-image: gradient( linear, left top, left bottom, from( #D3D9DB ), to( #D3D9DB ) ); + color: #4a4a4a; + margin: 0px 0px 0px 0px; + border: 1px solid #bdbdbd; + border-bottom: none; + border-left: none; +} + +TabItem:hover { + background-color: #82be1e; + background-gradient-color: #82be1e; + background-image: gradient( linear, left top, left bottom, from( #82be1e ), to( #82be1e ) ); + color: #4a4a4a; + margin: 1px 0px 0px -1px; + border-left: none; +} + +TabItem:selected:hover { + background-color: #82be1e; + background-gradient-color: #82be1e; + background-image: gradient( linear, left top, left bottom, from( #82be1e ), to( #82be1e ) ); + color: #4a4a4a; + margin: 0px 0px 0px 0px; + border-left: none; +} + +TabItem:first { + font: 12px Arial, Helvetica, sans-serif; + color: #4a4a4a; + background-color: #FFFFFF; + text-decoration: none; + text-shadow: none; + background-image: none; + margin: 1px 0px 0px -1px; + border: 1px solid #bdbdbd; + border-bottom: none; +} + +TabItem:first:hover { + background-color: #82be1e; + background-gradient-color: #82be1e; + background-image: gradient( linear, left top, left bottom, from( #82be1e ), to( #82be1e ) ); + color: #4a4a4a; + margin: 1px 0px 0px -1px; + border-left: none; +} + +TabItem:selected:first { + background-color: #D3D9DB; + color: #4a4a4a; + margin: 0px 0px 0px 0px; + border-left: 1px solid #bdbdbd; +} + +TabItem:selected:hover:first { + background-color: #82be1e; + background-gradient-color: #82be1e; + background-image: gradient( linear, left top, left bottom, from( #82be1e ), to( #82be1e ) ); + color: #4a4a4a; + margin: 0px 0px 0px 0px; + border-left: 1px solid #bdbdbd; +} + + +Widget-ToolTip { + padding: 10px 10px 10px 10px; + background-color: #D3D9DB; + border: 1px solid #3C5A0F; + border-radius: 1px 1px 1px 1px; + color: #4a4a4a; + opacity: 1; + animation: fadeIn 200ms linear, fadeOut 600ms ease-out; + box-shadow: 3px 4px 2px rgba(0, 0, 0, 0.3); + text-align: left; +} + +Widget-ToolTip-Pointer { + background-image: none; +} + +/* Table default theme */ +Table { + font: 12px Arial, Helvetica, sans-serif; + background-color: #ffffff; + background-image: none; + color: #4a4a4a; + border: none; +} + +Table[BORDER] { + border: 1px solid #bdbdbd; +} + +TableColumn { + font: 12px Arial, Helvetica, sans-serif; + background-color: #595959; + background-gradient-color: #595959; + background-image: gradient( linear, left top, left bottom, from( #595959 ), to( #595959 ) ); + padding: 4px 3px 4px 3px; + + color: #FFFFFF; + border-bottom: 1px solid #bdbdbd; + text-shadow: none; +} + +TableColumn:hover { + background-color: #595959; + background-gradient-color: #595959; + background-image: gradient( linear, left top, left bottom, from( #595959 ), to( #595959 ) ); +} + +TableItem { + background-color: transparent; + color: inherit; + text-decoration: none; + text-shadow: none; + background-image: none; +} + +Table-RowOverlay.warning { + background-color: rgba( 168, 50, 45, 0.5 ); + background-gradient-color: rgba( 168, 50, 45, 0.5 ); + background-image: gradient( linear, left top, left bottom, from(rgba( 168, 50, 45, 0.5 ) ), to( rgba( 168, 50, 45, 0.5 ) ) ); +} + +TableItem:linesvisible:even { + background-color: #ffffff; + color: inherit; +} + +Table-RowOverlay { + background-color: transparent; + color: inherit; + background-image: none; +} + + +Table-RowOverlay:hover { + color: #4a4a4a; + background-color: #b5b5b5; + background-image: gradient(linear, left top, left bottom, from(#b5b5b5), to(#b5b5b5)); +} + +Table-RowOverlay:selected { + color: #4a4a4a; + background-color: #c5c5c5; + background-image: gradient(linear, left top, left bottom, from(#c5c5c5),to(#c5c5c5)); +} + +Table-RowOverlay:selected:unfocused { + color: #4a4a4a; + background-color: #c5c5c5; + background-image: gradient(linear, left top, left bottom, from(#c5c5c5),to(#c5c5c5)); +} + +Table-RowOverlay:linesvisible:even:hover { + color: #4a4a4a; + background-color: #b5b5b5; + background-image: gradient(linear, left top, left bottom, from(#b5b5b5),to(#b5b5b5)); +} + +Table-RowOverlay:linesvisible:even:selected { + color: #4a4a4a; + background-color: #c5c5c5; + background-image: gradient(linear, left top, left bottom, from(#c5c5c5),to(#c5c5c5)); +} + +Table-RowOverlay:linesvisible:even:selected:unfocused { + background-color: #c5c5c5; + background-image: gradient(linear, left top, left bottom, from(#c5c5c5),to(#c5c5c5)); + color: #4a4a4a; +} + +TableColumn-SortIndicator { + background-image: none; +} +/* +TableColumn-SortIndicator:up { + background-image: url( themes/images/column/sort-indicator-up.png ); +} + +TableColumn-SortIndicator:down { + background-image: url( themes/images/column/sort-indicator-down.png ); +} +*/ + +Table-Cell { + spacing: 3px; + padding: 5px 3px 5px 3px; +} + +Table-GridLine, Table-GridLine:vertical:rowtemplate { + color: transparent; +} + +Table-GridLine:vertical, Table-GridLine:header, Table-GridLine:horizontal:rowtemplate { + color: transparent; +} + + + + + + diff --git a/src/main/resources/static/images/visibility.png b/src/main/resources/static/images/visibility.png new file mode 100644 index 0000000000000000000000000000000000000000..ff070f4dfe6412cd699e9101adcbc94b26b75ed2 GIT binary patch literal 255 zcmeAS@N?(olHy`uVBq!ia0vp^LLkh+1|-AI^@RheW1cRKAr*{oFEp|p3Xo`fXuzZB zz#*`}nsXsHhhmE&i-eOzTkHV_MTZwmY!Yn@&JB!AJcsXh>t~%l!|>*5BmeP_{Z-~B zM+M7mCO&GCjVZ}$7i!~CRC;0{60vYAgPOXa=O#?bn622*&l&T?Ao9_2 zL#`PzTaFui-0Es@^5K>K=tD;?*R^^s&|PDD#bO`-JrW2!dc3ieV@q7@!yciUA6M0tx^a00tOfC_1>{ zdZq~W^WdH1ymNWizoo%o29`Ksf?$CWFL*|a8y9ew!X}&x=L5I?pu!={5oSP-5&IOl zE{Cv7#z|cXR6sNfo>B(CB call = restService.getBuilder(NewClientConfig.class) - .withQueryParam(Domain.SEB_CLIENT_CONFIGURATION.ATTR_NAME, "new client config") - .call(); - - assertNotNull(call); - assertFalse(call.hasError()); - final SebClientConfig createdConfig = call.get(); - assertEquals(Long.valueOf(1), createdConfig.id); - assertEquals("new client config", createdConfig.name); - assertFalse(createdConfig.active); - } - - @Test - public void testNewClientConfigWithURLEncodedForm() { - final RestServiceImpl restService = createRestServiceForUser("admin", "admin", new NewClientConfig()); - - final Result call = restService.getBuilder(NewClientConfig.class) - .withFormParam(Domain.SEB_CLIENT_CONFIGURATION.ATTR_NAME, "new client config") - .call(); - - assertNotNull(call); - assertFalse(call.hasError()); - final SebClientConfig createdConfig = call.get(); - assertEquals(Long.valueOf(1), createdConfig.id); - assertEquals("new client config", createdConfig.name); - assertFalse(createdConfig.active); - } - - @Test - public void testCreate_Get_Activate_Save_Deactivate_ClientConfig() { - final RestServiceImpl restService = createRestServiceForUser("admin", "admin", - new NewClientConfig(), - new GetClientConfig(), - new ActivateClientConfig(), - new SaveClientConfig(), - new DeactivateClientConfig()); - - // create one - final SebClientConfig config = restService.getBuilder(NewClientConfig.class) - .withQueryParam(Domain.SEB_CLIENT_CONFIGURATION.ATTR_NAME, "new client config") - .call() - .getOrThrow(); - - // get - final Result call = restService.getBuilder(GetClientConfig.class) - .withURIVariable(API.PARAM_MODEL_ID, config.getModelId()) - .call(); - - assertNotNull(call); - assertFalse(call.hasError()); - final SebClientConfig createdConfig = call.get(); - assertEquals(config.id, createdConfig.id); - assertEquals("new client config", createdConfig.name); - assertFalse(createdConfig.active); - - // activate - final EntityProcessingReport activationReport = restService.getBuilder(ActivateClientConfig.class) - .withURIVariable(API.PARAM_MODEL_ID, config.getModelId()) - .call() - .getOrThrow(); - - assertTrue(activationReport.errors.isEmpty()); - assertEquals( - "EntityKey [modelId=1, entityType=SEB_CLIENT_CONFIGURATION]", - activationReport.getSingleSource().toString()); - - // save with password (no confirm) expecting validation error - final Result valError = restService.getBuilder(SaveClientConfig.class) - .withBody(new SebClientConfig( - config.id, - config.institutionId, - "new client config", - null, - null, - "password", - null, - null)) - .call(); - - assertTrue(valError.hasError()); - final Throwable error = valError.getError(); - assertTrue(error.getMessage().contains("confirm_encrypt_secret")); - assertTrue(error.getMessage().contains("password.mismatch")); - - // save with new password - final SebClientConfig newConfig = restService.getBuilder(SaveClientConfig.class) - .withBody(new SebClientConfig( - config.id, - config.institutionId, - "new client config", - null, - null, - "password", - "password", - null)) - .call() - .getOrThrow(); - - assertEquals(config.id, newConfig.id); - assertEquals("new client config", newConfig.name); - assertTrue(newConfig.active); - assertNull(newConfig.getEncryptSecret()); - - // deactivate - final EntityProcessingReport deactivationReport = restService.getBuilder(DeactivateClientConfig.class) - .withURIVariable(API.PARAM_MODEL_ID, config.getModelId()) - .call() - .getOrThrow(); - - assertTrue(deactivationReport.errors.isEmpty()); - assertEquals( - "EntityKey [modelId=1, entityType=SEB_CLIENT_CONFIGURATION]", - deactivationReport.getSingleSource().toString()); - } - -} +/* + * 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.integration; + +import static org.junit.Assert.*; + +import org.junit.Test; +import org.springframework.test.context.jdbc.Sql; + +import ch.ethz.seb.sebserver.gbl.api.API; +import ch.ethz.seb.sebserver.gbl.model.Domain; +import ch.ethz.seb.sebserver.gbl.model.EntityProcessingReport; +import ch.ethz.seb.sebserver.gbl.model.sebconfig.SebClientConfig; +import ch.ethz.seb.sebserver.gbl.util.Result; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestServiceImpl; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.clientconfig.ActivateClientConfig; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.clientconfig.DeactivateClientConfig; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.clientconfig.GetClientConfig; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.clientconfig.NewClientConfig; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.clientconfig.SaveClientConfig; + +@Sql(scripts = { "classpath:schema-test.sql", "classpath:data-test.sql" }) +public class ClientConfigTest extends GuiIntegrationTest { + + @Test + public void testNewClientConfigWithQueryParam() { + final RestServiceImpl restService = createRestServiceForUser("admin", "admin", new NewClientConfig()); + + final Result call = restService.getBuilder(NewClientConfig.class) + .withQueryParam(Domain.SEB_CLIENT_CONFIGURATION.ATTR_NAME, "new client config") + .withFormParam(SebClientConfig.ATTR_CONFIG_PURPOSE, SebClientConfig.ConfigPurpose.START_EXAM.name()) + .call(); + + assertNotNull(call); + assertFalse(call.hasError()); + final SebClientConfig createdConfig = call.get(); + assertEquals(Long.valueOf(1), createdConfig.id); + assertEquals("new client config", createdConfig.name); + assertFalse(createdConfig.active); + } + + @Test + public void testNewClientConfigWithURLEncodedForm() { + final RestServiceImpl restService = createRestServiceForUser("admin", "admin", new NewClientConfig()); + + final Result call = restService.getBuilder(NewClientConfig.class) + .withFormParam(Domain.SEB_CLIENT_CONFIGURATION.ATTR_NAME, "new client config") + .withFormParam(SebClientConfig.ATTR_CONFIG_PURPOSE, SebClientConfig.ConfigPurpose.START_EXAM.name()) + .call(); + + assertNotNull(call); + assertFalse(call.hasError()); + final SebClientConfig createdConfig = call.get(); + assertEquals(Long.valueOf(1), createdConfig.id); + assertEquals("new client config", createdConfig.name); + assertFalse(createdConfig.active); + } + + @Test + public void testCreate_Get_Activate_Save_Deactivate_ClientConfig() { + final RestServiceImpl restService = createRestServiceForUser("admin", "admin", + new NewClientConfig(), + new GetClientConfig(), + new ActivateClientConfig(), + new SaveClientConfig(), + new DeactivateClientConfig()); + + // create one + final SebClientConfig config = restService.getBuilder(NewClientConfig.class) + .withQueryParam(Domain.SEB_CLIENT_CONFIGURATION.ATTR_NAME, "new client config") + .withFormParam(SebClientConfig.ATTR_CONFIG_PURPOSE, SebClientConfig.ConfigPurpose.START_EXAM.name()) + .call() + .getOrThrow(); + + // get + final Result call = restService.getBuilder(GetClientConfig.class) + .withURIVariable(API.PARAM_MODEL_ID, config.getModelId()) + .call(); + + assertNotNull(call); + assertFalse(call.hasError()); + final SebClientConfig createdConfig = call.get(); + assertEquals(config.id, createdConfig.id); + assertEquals("new client config", createdConfig.name); + assertFalse(createdConfig.active); + + // activate + final EntityProcessingReport activationReport = restService.getBuilder(ActivateClientConfig.class) + .withURIVariable(API.PARAM_MODEL_ID, config.getModelId()) + .call() + .getOrThrow(); + + assertTrue(activationReport.errors.isEmpty()); + assertEquals( + "EntityKey [modelId=1, entityType=SEB_CLIENT_CONFIGURATION]", + activationReport.getSingleSource().toString()); + + // save with password (no confirm) expecting validation error + final Result valError = restService.getBuilder(SaveClientConfig.class) + .withBody(new SebClientConfig( + config.id, + config.institutionId, + "new client config", + SebClientConfig.ConfigPurpose.START_EXAM, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + "password", + null, + null)) + .call(); + + assertTrue(valError.hasError()); + final Throwable error = valError.getError(); + assertTrue(error.getMessage().contains("confirm_encrypt_secret")); + assertTrue(error.getMessage().contains("password.mismatch")); + + // save with new password + final SebClientConfig newConfig = restService.getBuilder(SaveClientConfig.class) + .withBody(new SebClientConfig( + config.id, + config.institutionId, + "new client config", + SebClientConfig.ConfigPurpose.START_EXAM, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + "password", + "password", + null)) + .call() + .getOrThrow(); + + assertEquals(config.id, newConfig.id); + assertEquals("new client config", newConfig.name); + assertTrue(newConfig.active); + assertNotNull(newConfig.getEncryptSecret()); + + // deactivate + final EntityProcessingReport deactivationReport = restService.getBuilder(DeactivateClientConfig.class) + .withURIVariable(API.PARAM_MODEL_ID, config.getModelId()) + .call() + .getOrThrow(); + + assertTrue(deactivationReport.errors.isEmpty()); + assertEquals( + "EntityKey [modelId=1, entityType=SEB_CLIENT_CONFIGURATION]", + deactivationReport.getSingleSource().toString()); + } + +} diff --git a/src/test/java/ch/ethz/seb/sebserver/gui/integration/UseCasesIntegrationTest.java b/src/test/java/ch/ethz/seb/sebserver/gui/integration/UseCasesIntegrationTest.java index 509d2d6b..332feccb 100644 --- a/src/test/java/ch/ethz/seb/sebserver/gui/integration/UseCasesIntegrationTest.java +++ b/src/test/java/ch/ethz/seb/sebserver/gui/integration/UseCasesIntegrationTest.java @@ -12,6 +12,7 @@ import static org.junit.Assert.*; import java.io.IOException; import java.io.InputStream; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Iterator; @@ -853,7 +854,12 @@ public class UseCasesIntegrationTest extends GuiIntegrationTest { final Result newConfigResponse = restService .getBuilder(NewClientConfig.class) .withFormParam(Domain.SEB_CLIENT_CONFIGURATION.ATTR_NAME, "No Password Protection") + .withFormParam(SebClientConfig.ATTR_FALLBACK, Constants.TRUE_STRING) .withFormParam(SebClientConfig.ATTR_FALLBACK_START_URL, "http://fallback.com/fallback") + .withFormParam(SebClientConfig.ATTR_FALLBACK_TIMEOUT, "100") + .withFormParam(SebClientConfig.ATTR_FALLBACK_ATTEMPTS, "5") + .withFormParam(SebClientConfig.ATTR_FALLBACK_ATTEMPT_INTERVAL, "5") + .withFormParam(SebClientConfig.ATTR_CONFIG_PURPOSE, SebClientConfig.ConfigPurpose.START_EXAM.name()) .call(); assertNotNull(newConfigResponse); @@ -886,9 +892,14 @@ public class UseCasesIntegrationTest extends GuiIntegrationTest { final Result configWithPasswordResponse = restService .getBuilder(NewClientConfig.class) .withFormParam(Domain.SEB_CLIENT_CONFIGURATION.ATTR_NAME, "With Password Protection") + .withFormParam(SebClientConfig.ATTR_CONFIG_PURPOSE, SebClientConfig.ConfigPurpose.START_EXAM.name()) + .withFormParam(SebClientConfig.ATTR_FALLBACK, Constants.TRUE_STRING) .withFormParam(SebClientConfig.ATTR_FALLBACK_START_URL, "http://fallback.com/fallback") + .withFormParam(SebClientConfig.ATTR_FALLBACK_TIMEOUT, "100") + .withFormParam(SebClientConfig.ATTR_FALLBACK_ATTEMPTS, "5") + .withFormParam(SebClientConfig.ATTR_FALLBACK_ATTEMPT_INTERVAL, "5") .withFormParam(SEB_CLIENT_CONFIGURATION.ATTR_ENCRYPT_SECRET, "123") - .withFormParam(SebClientConfig.ATTR_CONFIRM_ENCRYPT_SECRET, "123") + .withFormParam(SebClientConfig.ATTR_ENCRYPT_SECRET_CONFIRM, "123") .call(); assertNotNull(configWithPasswordResponse); @@ -1091,7 +1102,10 @@ public class UseCasesIntegrationTest extends GuiIntegrationTest { restService); // update a value -- grab first - final ConfigurationValue value = values.get(0); + ConfigurationValue value = values.get(0); + if (value.attributeId == 1) { + value = values.get(1); + } ConfigurationValue newValue = new ConfigurationValue( null, value.institutionId, value.configurationId, value.attributeId, value.listIndex, "2"); @@ -1188,8 +1202,9 @@ public class UseCasesIntegrationTest extends GuiIntegrationTest { assertNotNull(valuesResponse); assertFalse(valuesResponse.hasError()); values = valuesResponse.get(); + final ConfigurationValue _value = value; final ConfigurationValue currentValue = - values.stream().filter(v -> v.attributeId == value.attributeId).findFirst().orElse(null); + values.stream().filter(v -> v.attributeId == _value.attributeId).findFirst().orElse(null); assertNotNull(currentValue); assertEquals("2", currentValue.value); } @@ -1336,12 +1351,10 @@ public class UseCasesIntegrationTest extends GuiIntegrationTest { new GetFollowupConfiguration()); // get all configuration attributes - final Collection attributes = restService + final Collection attributes = new ArrayList<>(restService .getBuilder(GetConfigAttributes.class) .call() - .getOrThrow() - .stream() - .collect(Collectors.toList()); + .getOrThrow()); // get configuration page final Result> pageResponse = restService diff --git a/src/test/java/ch/ethz/seb/sebserver/gui/integration/UsecaseTestUtils.java b/src/test/java/ch/ethz/seb/sebserver/gui/integration/UsecaseTestUtils.java index e25d63ef..7e7519e7 100644 --- a/src/test/java/ch/ethz/seb/sebserver/gui/integration/UsecaseTestUtils.java +++ b/src/test/java/ch/ethz/seb/sebserver/gui/integration/UsecaseTestUtils.java @@ -1,132 +1,134 @@ -/* - * Copyright (c) 2020 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.integration; - -import static org.junit.Assert.*; - -import java.util.Map; -import java.util.function.Function; -import java.util.stream.Collectors; - -import org.apache.tomcat.util.buf.StringUtils; - -import ch.ethz.seb.sebserver.gbl.Constants; -import ch.ethz.seb.sebserver.gbl.model.Domain; -import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationAttribute; -import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationTableValues; -import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestServiceImpl; -import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.examconfig.GetConfigAttributes; -import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.examconfig.GetConfigurationTableValues; -import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.impl.init.XMLAttributeLoader; - -public abstract class UsecaseTestUtils { - - static ConfigurationTableValues testProhibitedProcessesInit( - final String configId, - final RestServiceImpl restService) { - - final ConfigurationTableValues tableValues = getTableValues("93", configId, restService); - - assertNotNull(tableValues); - assertFalse(tableValues.values.isEmpty()); - final String names = StringUtils.join( - tableValues.values - .stream() - .filter(attr -> attr.attributeId == 98) - .map(attr -> attr.value) - .sorted() - .collect(Collectors.toList()), - Constants.LIST_SEPARATOR_CHAR); - - // get all configuration attributes - final Map attributes = restService - .getBuilder(GetConfigAttributes.class) - .call() - .getOrThrow() - .stream() - .collect(Collectors.toMap(attr -> attr.name, Function.identity())); - - final XMLAttributeLoader xmlAttributeLoader = new XMLAttributeLoader(); - final String configuraedNames = StringUtils.join(xmlAttributeLoader.loadFromXML( - 1L, - Long.parseLong(configId), - attrName -> attributes.get(attrName), - "config/initialProhibitedProcesses.xml") - .stream() - .filter(attr -> attr.attributeId == 98) - .map(attr -> attr.value) - .sorted() - .collect(Collectors.toList()), - Constants.LIST_SEPARATOR_CHAR); - - assertEquals(configuraedNames, names); - - return tableValues; - } - - static ConfigurationTableValues getTableValues( - final String attributeId, - final String configId, - final RestServiceImpl restService) { - final ConfigurationTableValues tableValues = restService.getBuilder(GetConfigurationTableValues.class) - .withQueryParam( - Domain.CONFIGURATION_VALUE.ATTR_CONFIGURATION_ATTRIBUTE_ID, - attributeId) - .withQueryParam( - Domain.CONFIGURATION_VALUE.ATTR_CONFIGURATION_ID, - configId) - .call() - .getOrThrow(); - return tableValues; - } - - static ConfigurationTableValues testPermittedProcessesInit( - final String configId, - final RestServiceImpl restService) { - - final ConfigurationTableValues tableValues = getTableValues("73", configId, restService); - - assertNotNull(tableValues); - assertFalse(tableValues.values.isEmpty()); - final String names = StringUtils.join( - tableValues.values - .stream() - .filter(attr -> attr.attributeId == 76) - .map(attr -> attr.value) - .sorted() - .collect(Collectors.toList()), - Constants.LIST_SEPARATOR_CHAR); - - // get all configuration attributes - final Map attributes = restService - .getBuilder(GetConfigAttributes.class) - .call() - .getOrThrow() - .stream() - .collect(Collectors.toMap(attr -> attr.name, Function.identity())); - - final XMLAttributeLoader xmlAttributeLoader = new XMLAttributeLoader(); - final String configuraedNames = StringUtils.join(xmlAttributeLoader.loadFromXML( - 1L, - Long.parseLong(configId), - attrName -> attributes.get(attrName), - "config/initialPermittedProcesses.xml") - .stream() - .filter(attr -> attr.attributeId == 76) - .map(attr -> attr.value) - .sorted() - .collect(Collectors.toList()), - Constants.LIST_SEPARATOR_CHAR); - - assertEquals(configuraedNames, names); - - return tableValues; - } - -} +/* + * Copyright (c) 2020 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.integration; + +import static org.junit.Assert.*; + +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +import ch.ethz.seb.sebserver.gbl.util.Cryptor; +import org.apache.tomcat.util.buf.StringUtils; + +import ch.ethz.seb.sebserver.gbl.Constants; +import ch.ethz.seb.sebserver.gbl.model.Domain; +import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationAttribute; +import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationTableValues; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestServiceImpl; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.examconfig.GetConfigAttributes; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.examconfig.GetConfigurationTableValues; +import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.impl.init.XMLAttributeLoader; +import org.mockito.Mockito; + +public abstract class UsecaseTestUtils { + + static ConfigurationTableValues testProhibitedProcessesInit( + final String configId, + final RestServiceImpl restService) { + + final ConfigurationTableValues tableValues = getTableValues("93", configId, restService); + + assertNotNull(tableValues); + assertFalse(tableValues.values.isEmpty()); + final String names = StringUtils.join( + tableValues.values + .stream() + .filter(attr -> attr.attributeId == 98) + .map(attr -> attr.value) + .sorted() + .collect(Collectors.toList()), + Constants.LIST_SEPARATOR_CHAR); + + // get all configuration attributes + final Map attributes = restService + .getBuilder(GetConfigAttributes.class) + .call() + .getOrThrow() + .stream() + .collect(Collectors.toMap(attr -> attr.name, Function.identity())); + + final XMLAttributeLoader xmlAttributeLoader = new XMLAttributeLoader(Mockito.mock(Cryptor.class)); + final String configuraedNames = StringUtils.join(xmlAttributeLoader.loadFromXML( + 1L, + Long.parseLong(configId), + attrName -> attributes.get(attrName), + "config/initialProhibitedProcesses.xml") + .stream() + .filter(attr -> attr.attributeId == 98) + .map(attr -> attr.value) + .sorted() + .collect(Collectors.toList()), + Constants.LIST_SEPARATOR_CHAR); + + assertEquals(configuraedNames, names); + + return tableValues; + } + + static ConfigurationTableValues getTableValues( + final String attributeId, + final String configId, + final RestServiceImpl restService) { + final ConfigurationTableValues tableValues = restService.getBuilder(GetConfigurationTableValues.class) + .withQueryParam( + Domain.CONFIGURATION_VALUE.ATTR_CONFIGURATION_ATTRIBUTE_ID, + attributeId) + .withQueryParam( + Domain.CONFIGURATION_VALUE.ATTR_CONFIGURATION_ID, + configId) + .call() + .getOrThrow(); + return tableValues; + } + + static ConfigurationTableValues testPermittedProcessesInit( + final String configId, + final RestServiceImpl restService) { + + final ConfigurationTableValues tableValues = getTableValues("73", configId, restService); + + assertNotNull(tableValues); + assertFalse(tableValues.values.isEmpty()); + final String names = StringUtils.join( + tableValues.values + .stream() + .filter(attr -> attr.attributeId == 76) + .map(attr -> attr.value) + .sorted() + .collect(Collectors.toList()), + Constants.LIST_SEPARATOR_CHAR); + + // get all configuration attributes + final Map attributes = restService + .getBuilder(GetConfigAttributes.class) + .call() + .getOrThrow() + .stream() + .collect(Collectors.toMap(attr -> attr.name, Function.identity())); + + final XMLAttributeLoader xmlAttributeLoader = new XMLAttributeLoader(Mockito.mock(Cryptor.class)); + final String configuraedNames = StringUtils.join(xmlAttributeLoader.loadFromXML( + 1L, + Long.parseLong(configId), + attrName -> attributes.get(attrName), + "config/initialPermittedProcesses.xml") + .stream() + .filter(attr -> attr.attributeId == 76) + .map(attr -> attr.value) + .sorted() + .collect(Collectors.toList()), + Constants.LIST_SEPARATOR_CHAR); + + assertEquals(configuraedNames, names); + + return tableValues; + } + +} diff --git a/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/client/ClientCredentialServiceTest.java b/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/client/ClientCredentialServiceTest.java index b07e06ae..01384d7e 100644 --- a/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/client/ClientCredentialServiceTest.java +++ b/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/client/ClientCredentialServiceTest.java @@ -1,55 +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.webservice.servicelayer.client; - -import static org.junit.Assert.assertEquals; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import org.junit.Test; -import org.springframework.core.env.Environment; - -public class ClientCredentialServiceTest { - -// @Test -// public void testEncryptSimpleSecret() { -// final Environment envMock = mock(Environment.class); -// when(envMock.getProperty(ClientCredentialServiceImpl.SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY)) -// .thenReturn("somePW"); -// -// final ClientCredentialService service = new ClientCredentialServiceImpl(envMock); -// final CharSequence encrypt = service.encrypt("test"); -// assertEquals("", encrypt.toString()); -// } - - @Test - public void testEncryptDecryptClientCredentials() { - final Environment envMock = mock(Environment.class); - when(envMock.getRequiredProperty(ClientCredentialServiceImpl.SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY)) - .thenReturn("secret1"); - - final String clientName = "simpleClientName"; - - final ClientCredentialServiceImpl service = new ClientCredentialServiceImpl(envMock); - String encrypted = - service.encrypt(clientName, "secret1").toString(); - String decrypted = service.decrypt(encrypted, "secret1").toString(); - - assertEquals(clientName, decrypted); - - final String clientSecret = "fbjreij39ru29305ruࣣàèLöäöäü65%(/%(ç87"; - - encrypted = - service.encrypt(clientSecret, "secret1").toString(); - decrypted = service.decrypt(encrypted, "secret1").toString(); - - assertEquals(clientSecret, decrypted); - } - -} +/* + * 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.client; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import ch.ethz.seb.sebserver.gbl.util.Cryptor; +import org.junit.Test; +import org.springframework.core.env.Environment; + +public class ClientCredentialServiceTest { + +// @Test +// public void testEncryptSimpleSecret() { +// final Environment envMock = mock(Environment.class); +// when(envMock.getProperty(ClientCredentialServiceImpl.SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY)) +// .thenReturn("somePW"); +// +// final ClientCredentialService service = new ClientCredentialServiceImpl(envMock); +// final CharSequence encrypt = service.encrypt("test"); +// assertEquals("", encrypt.toString()); +// } + + @Test + public void testEncryptDecryptClientCredentials() { + final Environment envMock = mock(Environment.class); + when(envMock.getRequiredProperty(Cryptor.SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY)) + .thenReturn("secret1"); + + Cryptor cryptor = new Cryptor(envMock); + + final String clientName = "simpleClientName"; + + final ClientCredentialServiceImpl service = new ClientCredentialServiceImpl(envMock, cryptor); + String encrypted = + cryptor.encrypt(clientName, "secret1").toString(); + String decrypted = cryptor.decrypt(encrypted, "secret1").toString(); + + assertEquals(clientName, decrypted); + + final String clientSecret = "fbjreij39ru29305ruࣣàèLöäöäü65%(/%(ç87"; + + encrypted = + cryptor.encrypt(clientSecret, "secret1").toString(); + decrypted = cryptor.decrypt(encrypted, "secret1").toString(); + + assertEquals(clientSecret, decrypted); + } + +} diff --git a/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/ExamConfigImportHandlerTest.java b/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/ExamConfigImportHandlerTest.java index df56431f..7e689211 100644 --- a/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/ExamConfigImportHandlerTest.java +++ b/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/ExamConfigImportHandlerTest.java @@ -1,339 +1,347 @@ -/* - * 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.Test; - -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.ConfigurationValue; - -public class ExamConfigImportHandlerTest { - - private static final Function attributeResolver = - name -> new ConfigurationAttribute( - getId(name), - null, name, (name.contains("array")) ? AttributeType.MULTI_SELECTION : null, null, null, null, - null); - - private static final Long getId(final String name) { - try { - return Long.parseLong(String.valueOf(name.charAt(name.length() - 1))); - } catch (final Exception e) { - return -1L; - } - } - - @Test - public void simpleStringValueTest() throws Exception { - final ValueCollector valueCollector = new ValueCollector(); - final ExamConfigXMLParser candidate = new ExamConfigXMLParser( - 1L, - 1L, - valueCollector, - attributeResolver); - - 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 ExamConfigXMLParser candidate = new ExamConfigXMLParser( - 1L, - 1L, - valueCollector, - attributeResolver); - - 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 ExamConfigXMLParser candidate = new ExamConfigXMLParser( - 1L, - 1L, - valueCollector, - attributeResolver); - - 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 ExamConfigXMLParser candidate = new ExamConfigXMLParser( - 1L, - 1L, - valueCollector, - attributeResolver); - - 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() == 1); - final ConfigurationValue configurationValue1 = valueCollector.values.get(0); - assertEquals("val1,val2,val3", configurationValue1.value); - assertTrue(configurationValue1.listIndex == 0); - - } - - @Test - public void dictOfValuesTest() throws Exception { - final ValueCollector valueCollector = new ValueCollector(); - final List attrNamesCollector = new ArrayList<>(); - final Function attrConverter = attrName -> { - attrNamesCollector.add(attrName); - return attributeResolver.apply(attrName); - }; - final ExamConfigXMLParser candidate = new ExamConfigXMLParser( - 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 attributeResolver.apply(attrName); - }; - final ExamConfigXMLParser candidate = new ExamConfigXMLParser( - 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, attribute]", - attrNamesCollector.toString()); - } - - private static final class ValueCollector implements Consumer { - List values = new ArrayList<>(); - - @Override - public void accept(final ConfigurationValue value) { - this.values.add(value); - } - } -} +/* + * 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 ch.ethz.seb.sebserver.gbl.util.Cryptor; +import org.junit.Test; + +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.ConfigurationValue; +import org.mockito.Mockito; + +public class ExamConfigImportHandlerTest { + + private static final Function attributeResolver = + name -> new ConfigurationAttribute( + getId(name), + null, name, (name.contains("array")) ? AttributeType.MULTI_SELECTION : null, null, null, null, + null); + + private static final Long getId(final String name) { + try { + return Long.parseLong(String.valueOf(name.charAt(name.length() - 1))); + } catch (final Exception e) { + return -1L; + } + } + + @Test + public void simpleStringValueTest() throws Exception { + final ValueCollector valueCollector = new ValueCollector(); + final ExamConfigXMLParser candidate = new ExamConfigXMLParser( + Mockito.mock(Cryptor.class), + 1L, + 1L, + valueCollector, + attributeResolver); + + 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 ExamConfigXMLParser candidate = new ExamConfigXMLParser( + Mockito.mock(Cryptor.class), + 1L, + 1L, + valueCollector, + attributeResolver); + + 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 ExamConfigXMLParser candidate = new ExamConfigXMLParser( + Mockito.mock(Cryptor.class), + 1L, + 1L, + valueCollector, + attributeResolver); + + 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 ExamConfigXMLParser candidate = new ExamConfigXMLParser( + Mockito.mock(Cryptor.class), + 1L, + 1L, + valueCollector, + attributeResolver); + + 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() == 1); + final ConfigurationValue configurationValue1 = valueCollector.values.get(0); + assertEquals("val1,val2,val3", configurationValue1.value); + assertTrue(configurationValue1.listIndex == 0); + + } + + @Test + public void dictOfValuesTest() throws Exception { + final ValueCollector valueCollector = new ValueCollector(); + final List attrNamesCollector = new ArrayList<>(); + final Function attrConverter = attrName -> { + attrNamesCollector.add(attrName); + return attributeResolver.apply(attrName); + }; + final ExamConfigXMLParser candidate = new ExamConfigXMLParser( + Mockito.mock(Cryptor.class), + 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 attributeResolver.apply(attrName); + }; + final ExamConfigXMLParser candidate = new ExamConfigXMLParser( + Mockito.mock(Cryptor.class), + 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, attribute]", + 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/converter/TableConverterTest.java b/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/converter/TableConverterTest.java index db92ba0c..a45490d3 100644 --- a/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/converter/TableConverterTest.java +++ b/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/converter/TableConverterTest.java @@ -1,328 +1,330 @@ -/* - * 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.converter; - -import static org.junit.Assert.assertEquals; - -import java.io.ByteArrayOutputStream; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.List; - -import org.junit.Test; -import org.mockito.Mockito; - -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.ConfigurationValue; -import ch.ethz.seb.sebserver.gbl.util.Result; -import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationAttributeDAO; -import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationValueDAO; -import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.AttributeValueConverter; -import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.AttributeValueConverterService; - -public class TableConverterTest { - - // ******************************** - // **** table - // ******************************** - private final ConfigurationAttribute TABLE_ATTR = - new ConfigurationAttribute(1L, null, "table", AttributeType.TABLE, null, null, null, null); - private final ConfigurationValue TABLE_VALUE = - new ConfigurationValue(1L, 1L, 1L, 1L, 0, null); - - private final ConfigurationAttribute COLUMN_ATTR_1 = - new ConfigurationAttribute(2L, 1L, "attr1", AttributeType.TEXT_FIELD, null, null, null, null); - private final ConfigurationAttribute COLUMN_ATTR_2 = - new ConfigurationAttribute(3L, 1L, "attr2", AttributeType.TEXT_FIELD, null, null, null, null); - - private final ConfigurationValue ROW_1_ATTR_1 = - new ConfigurationValue(2L, 1L, 1L, 2L, 0, "1"); - private final ConfigurationValue ROW_1_ATTR_2 = - new ConfigurationValue(3L, 1L, 1L, 3L, 0, "2"); - - private final ConfigurationValue ROW_2_ATTR_1 = - new ConfigurationValue(4L, 1L, 1L, 2L, 1, "3"); - private final ConfigurationValue ROW_2_ATTR_2 = - new ConfigurationValue(5L, 1L, 1L, 3L, 1, "4"); - - private final Collection TABLE_COLUMNS = Arrays.asList( - this.COLUMN_ATTR_1, - this.COLUMN_ATTR_2); - - private final List> TABLE_VALUES = Arrays.asList( - Arrays.asList(this.ROW_1_ATTR_1, this.ROW_1_ATTR_2), - Arrays.asList(this.ROW_2_ATTR_1, this.ROW_2_ATTR_2)); - - // ******************************** - // **** Composite table - // ******************************** - private final ConfigurationAttribute COMPOSITE_TABLE_ATTR = - new ConfigurationAttribute(1L, null, "table", AttributeType.COMPOSITE_TABLE, null, null, null, null); - private final ConfigurationValue COMPOSITE_TABLE_VALUE = - new ConfigurationValue(1L, 1L, 1L, 1L, 0, null); - - private final ConfigurationAttribute COMPOSITE_COLUMN_ATTR_1 = - new ConfigurationAttribute(2L, 1L, "attr1", AttributeType.TEXT_FIELD, null, null, null, null); - private final ConfigurationAttribute COMPOSITE_COLUMN_ATTR_2 = - new ConfigurationAttribute(3L, 1L, "attr2", AttributeType.TEXT_FIELD, null, null, null, null); - - private final ConfigurationValue COMPOSITE_ROW_1_ATTR_1 = - new ConfigurationValue(2L, 1L, 1L, 2L, 0, "1"); - private final ConfigurationValue COMPOSITE_ROW_1_ATTR_2 = - new ConfigurationValue(3L, 1L, 1L, 3L, 0, "2"); - - private final Collection COMPOSITE_TABLE_ENTRIES = Arrays.asList( - this.COMPOSITE_COLUMN_ATTR_1, - this.COMPOSITE_COLUMN_ATTR_2); - - private final List> COMPOSITE_TABLE_VALUES = Arrays.asList( - Arrays.asList(this.COMPOSITE_ROW_1_ATTR_1, this.COMPOSITE_ROW_1_ATTR_2)); - - @Test - public void testXMLNormalTable() throws Exception { - - final ConfigurationAttributeDAO configurationAttributeDAO = - Mockito.mock(ConfigurationAttributeDAO.class); - Mockito.when(configurationAttributeDAO.allMatching(Mockito.any())) - .thenReturn(Result.of(this.TABLE_COLUMNS)); - - final ConfigurationValueDAO configurationValueDAO = - Mockito.mock(ConfigurationValueDAO.class); - Mockito.when(configurationValueDAO.getOrderedTableValues(1L, 1L, 1L)) - .thenReturn(Result.of(this.TABLE_VALUES)); - - final TableConverter tableConverter = new TableConverter(configurationAttributeDAO, configurationValueDAO); - tableConverter.init(createAttributeValueConverterService()); - final ByteArrayOutputStream out = new ByteArrayOutputStream(); - - tableConverter.convertToXML(out, this.TABLE_ATTR, attr -> this.TABLE_VALUE); - - final String xmlString = new String(out.toByteArray()); - assertEquals( - "table" - + "" - + "" - + "attr1" - + "1" - + "attr2" - + "2" - + "" - + "" - + "attr1" - + "3" - + "attr2" - + "4" - + "" - + "", - xmlString); - - } - - @Test - public void testXMLNormalTableNoValues() throws Exception { - - final ConfigurationAttributeDAO configurationAttributeDAO = - Mockito.mock(ConfigurationAttributeDAO.class); - Mockito.when(configurationAttributeDAO.allMatching(Mockito.any())) - .thenReturn(Result.of(this.TABLE_COLUMNS)); - - final ConfigurationValueDAO configurationValueDAO = - Mockito.mock(ConfigurationValueDAO.class); - Mockito.when(configurationValueDAO.getOrderedTableValues(1L, 1L, 1L)) - .thenReturn(Result.of(Collections.emptyList())); - - final TableConverter tableConverter = new TableConverter(configurationAttributeDAO, configurationValueDAO); - tableConverter.init(createAttributeValueConverterService()); - final ByteArrayOutputStream out = new ByteArrayOutputStream(); - - tableConverter.convertToXML(out, this.TABLE_ATTR, attr -> this.TABLE_VALUE); - - final String xmlString = new String(out.toByteArray()); - assertEquals( - "table", - xmlString); - - } - - @Test - public void testXMLCompositeTable() throws Exception { - - final ConfigurationAttributeDAO configurationAttributeDAO = - Mockito.mock(ConfigurationAttributeDAO.class); - Mockito.when(configurationAttributeDAO.allMatching(Mockito.any())) - .thenReturn(Result.of(this.COMPOSITE_TABLE_ENTRIES)); - - final ConfigurationValueDAO configurationValueDAO = - Mockito.mock(ConfigurationValueDAO.class); - Mockito.when(configurationValueDAO.getOrderedTableValues(1L, 1L, 1L)) - .thenReturn(Result.of(this.COMPOSITE_TABLE_VALUES)); - - final TableConverter tableConverter = new TableConverter(configurationAttributeDAO, configurationValueDAO); - tableConverter.init(createAttributeValueConverterService()); - final ByteArrayOutputStream out = new ByteArrayOutputStream(); - - tableConverter.convertToXML(out, this.COMPOSITE_TABLE_ATTR, attr -> this.COMPOSITE_TABLE_VALUE); - - final String xmlString = new String(out.toByteArray()); - assertEquals( - "table" - + "" - + "attr1" - + "1" - + "attr2" - + "2" - + "", - xmlString); - - } - - @Test - public void testXMLCompositeTableEmpty() throws Exception { - - final ConfigurationAttributeDAO configurationAttributeDAO = - Mockito.mock(ConfigurationAttributeDAO.class); - Mockito.when(configurationAttributeDAO.allMatching(Mockito.any())) - .thenReturn(Result.of(this.COMPOSITE_TABLE_ENTRIES)); - - final ConfigurationValueDAO configurationValueDAO = - Mockito.mock(ConfigurationValueDAO.class); - Mockito.when(configurationValueDAO.getOrderedTableValues(1L, 1L, 1L)) - .thenReturn(Result.of(Collections.emptyList())); - - final TableConverter tableConverter = new TableConverter(configurationAttributeDAO, configurationValueDAO); - tableConverter.init(createAttributeValueConverterService()); - final ByteArrayOutputStream out = new ByteArrayOutputStream(); - - tableConverter.convertToXML(out, this.COMPOSITE_TABLE_ATTR, attr -> this.COMPOSITE_TABLE_VALUE); - - final String xmlString = new String(out.toByteArray()); - assertEquals( - "", - xmlString); - - } - - @Test - public void testJSONNormalTable() throws Exception { - - final ConfigurationAttributeDAO configurationAttributeDAO = - Mockito.mock(ConfigurationAttributeDAO.class); - Mockito.when(configurationAttributeDAO.allMatching(Mockito.any())) - .thenReturn(Result.of(this.TABLE_COLUMNS)); - - final ConfigurationValueDAO configurationValueDAO = - Mockito.mock(ConfigurationValueDAO.class); - Mockito.when(configurationValueDAO.getOrderedTableValues(1L, 1L, 1L)) - .thenReturn(Result.of(this.TABLE_VALUES)); - - final TableConverter tableConverter = new TableConverter(configurationAttributeDAO, configurationValueDAO); - tableConverter.init(createAttributeValueConverterService()); - final ByteArrayOutputStream out = new ByteArrayOutputStream(); - - tableConverter.convertToJSON(out, this.TABLE_ATTR, attr -> this.TABLE_VALUE); - - final String xmlString = new String(out.toByteArray()); - // expected : "table":[{"attr1":"1","attr2":"2"},{"attr1":"3","attr2":"4"}] - assertEquals( - "\"table\":[{\"attr1\":\"1\",\"attr2\":\"2\"},{\"attr1\":\"3\",\"attr2\":\"4\"}]", - xmlString); - - } - - @Test - public void testJSONNormalTableEmpty() throws Exception { - - final ConfigurationAttributeDAO configurationAttributeDAO = - Mockito.mock(ConfigurationAttributeDAO.class); - Mockito.when(configurationAttributeDAO.allMatching(Mockito.any())) - .thenReturn(Result.of(this.TABLE_COLUMNS)); - - final ConfigurationValueDAO configurationValueDAO = - Mockito.mock(ConfigurationValueDAO.class); - Mockito.when(configurationValueDAO.getOrderedTableValues(1L, 1L, 1L)) - .thenReturn(Result.of(Collections.emptyList())); - - final TableConverter tableConverter = new TableConverter(configurationAttributeDAO, configurationValueDAO); - tableConverter.init(createAttributeValueConverterService()); - final ByteArrayOutputStream out = new ByteArrayOutputStream(); - - tableConverter.convertToJSON(out, this.TABLE_ATTR, attr -> this.TABLE_VALUE); - - final String xmlString = new String(out.toByteArray()); - // expected : "table":[] - assertEquals( - "\"table\":[]", - xmlString); - - } - - @Test - public void testJSONCompositeTable() throws Exception { - - final ConfigurationAttributeDAO configurationAttributeDAO = - Mockito.mock(ConfigurationAttributeDAO.class); - Mockito.when(configurationAttributeDAO.allMatching(Mockito.any())) - .thenReturn(Result.of(this.COMPOSITE_TABLE_ENTRIES)); - - final ConfigurationValueDAO configurationValueDAO = - Mockito.mock(ConfigurationValueDAO.class); - Mockito.when(configurationValueDAO.getOrderedTableValues(1L, 1L, 1L)) - .thenReturn(Result.of(this.COMPOSITE_TABLE_VALUES)); - - final TableConverter tableConverter = new TableConverter(configurationAttributeDAO, configurationValueDAO); - tableConverter.init(createAttributeValueConverterService()); - final ByteArrayOutputStream out = new ByteArrayOutputStream(); - - tableConverter.convertToJSON(out, this.COMPOSITE_TABLE_ATTR, attr -> this.COMPOSITE_TABLE_VALUE); - - final String xmlString = new String(out.toByteArray()); - // expected : "table":{"attr1":"1","attr2":"2"} - assertEquals( - "\"table\":{\"attr1\":\"1\",\"attr2\":\"2\"}", - xmlString); - - } - - @Test - public void testJSONCompositeTableEmpty() throws Exception { - - final ConfigurationAttributeDAO configurationAttributeDAO = - Mockito.mock(ConfigurationAttributeDAO.class); - Mockito.when(configurationAttributeDAO.allMatching(Mockito.any())) - .thenReturn(Result.of(this.COMPOSITE_TABLE_ENTRIES)); - - final ConfigurationValueDAO configurationValueDAO = - Mockito.mock(ConfigurationValueDAO.class); - Mockito.when(configurationValueDAO.getOrderedTableValues(1L, 1L, 1L)) - .thenReturn(Result.of(Collections.emptyList())); - - final TableConverter tableConverter = new TableConverter(configurationAttributeDAO, configurationValueDAO); - tableConverter.init(createAttributeValueConverterService()); - final ByteArrayOutputStream out = new ByteArrayOutputStream(); - - tableConverter.convertToJSON(out, this.COMPOSITE_TABLE_ATTR, attr -> this.COMPOSITE_TABLE_VALUE); - - final String xmlString = new String(out.toByteArray()); - // expected : - assertEquals( - "", - xmlString); - - } - - private AttributeValueConverterService createAttributeValueConverterService() { - final List converter = new ArrayList<>(); - converter.add(new StringConverter()); - return new AttributeValueConverterServiceImpl(converter); - } - -} +/* + * 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.converter; + +import static org.junit.Assert.assertEquals; + +import java.io.ByteArrayOutputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentialService; +import org.junit.Test; +import org.mockito.Mockito; + +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.ConfigurationValue; +import ch.ethz.seb.sebserver.gbl.util.Result; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationAttributeDAO; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationValueDAO; +import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.AttributeValueConverter; +import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.AttributeValueConverterService; + +public class TableConverterTest { + + // ******************************** + // **** table + // ******************************** + private final ConfigurationAttribute TABLE_ATTR = + new ConfigurationAttribute(1L, null, "table", AttributeType.TABLE, null, null, null, null); + private final ConfigurationValue TABLE_VALUE = + new ConfigurationValue(1L, 1L, 1L, 1L, 0, null); + + private final ConfigurationAttribute COLUMN_ATTR_1 = + new ConfigurationAttribute(2L, 1L, "attr1", AttributeType.TEXT_FIELD, null, null, null, null); + private final ConfigurationAttribute COLUMN_ATTR_2 = + new ConfigurationAttribute(3L, 1L, "attr2", AttributeType.TEXT_FIELD, null, null, null, null); + + private final ConfigurationValue ROW_1_ATTR_1 = + new ConfigurationValue(2L, 1L, 1L, 2L, 0, "1"); + private final ConfigurationValue ROW_1_ATTR_2 = + new ConfigurationValue(3L, 1L, 1L, 3L, 0, "2"); + + private final ConfigurationValue ROW_2_ATTR_1 = + new ConfigurationValue(4L, 1L, 1L, 2L, 1, "3"); + private final ConfigurationValue ROW_2_ATTR_2 = + new ConfigurationValue(5L, 1L, 1L, 3L, 1, "4"); + + private final Collection TABLE_COLUMNS = Arrays.asList( + this.COLUMN_ATTR_1, + this.COLUMN_ATTR_2); + + private final List> TABLE_VALUES = Arrays.asList( + Arrays.asList(this.ROW_1_ATTR_1, this.ROW_1_ATTR_2), + Arrays.asList(this.ROW_2_ATTR_1, this.ROW_2_ATTR_2)); + + // ******************************** + // **** Composite table + // ******************************** + private final ConfigurationAttribute COMPOSITE_TABLE_ATTR = + new ConfigurationAttribute(1L, null, "table", AttributeType.COMPOSITE_TABLE, null, null, null, null); + private final ConfigurationValue COMPOSITE_TABLE_VALUE = + new ConfigurationValue(1L, 1L, 1L, 1L, 0, null); + + private final ConfigurationAttribute COMPOSITE_COLUMN_ATTR_1 = + new ConfigurationAttribute(2L, 1L, "attr1", AttributeType.TEXT_FIELD, null, null, null, null); + private final ConfigurationAttribute COMPOSITE_COLUMN_ATTR_2 = + new ConfigurationAttribute(3L, 1L, "attr2", AttributeType.TEXT_FIELD, null, null, null, null); + + private final ConfigurationValue COMPOSITE_ROW_1_ATTR_1 = + new ConfigurationValue(2L, 1L, 1L, 2L, 0, "1"); + private final ConfigurationValue COMPOSITE_ROW_1_ATTR_2 = + new ConfigurationValue(3L, 1L, 1L, 3L, 0, "2"); + + private final Collection COMPOSITE_TABLE_ENTRIES = Arrays.asList( + this.COMPOSITE_COLUMN_ATTR_1, + this.COMPOSITE_COLUMN_ATTR_2); + + private final List> COMPOSITE_TABLE_VALUES = Arrays.asList( + Arrays.asList(this.COMPOSITE_ROW_1_ATTR_1, this.COMPOSITE_ROW_1_ATTR_2)); + + @Test + public void testXMLNormalTable() throws Exception { + + final ConfigurationAttributeDAO configurationAttributeDAO = + Mockito.mock(ConfigurationAttributeDAO.class); + Mockito.when(configurationAttributeDAO.allMatching(Mockito.any())) + .thenReturn(Result.of(this.TABLE_COLUMNS)); + + final ConfigurationValueDAO configurationValueDAO = + Mockito.mock(ConfigurationValueDAO.class); + Mockito.when(configurationValueDAO.getOrderedTableValues(1L, 1L, 1L)) + .thenReturn(Result.of(this.TABLE_VALUES)); + + final TableConverter tableConverter = new TableConverter(configurationAttributeDAO, configurationValueDAO); + tableConverter.init(createAttributeValueConverterService()); + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + + tableConverter.convertToXML(out, this.TABLE_ATTR, attr -> this.TABLE_VALUE); + + final String xmlString = new String(out.toByteArray()); + assertEquals( + "table" + + "" + + "" + + "attr1" + + "1" + + "attr2" + + "2" + + "" + + "" + + "attr1" + + "3" + + "attr2" + + "4" + + "" + + "", + xmlString); + + } + + @Test + public void testXMLNormalTableNoValues() throws Exception { + + final ConfigurationAttributeDAO configurationAttributeDAO = + Mockito.mock(ConfigurationAttributeDAO.class); + Mockito.when(configurationAttributeDAO.allMatching(Mockito.any())) + .thenReturn(Result.of(this.TABLE_COLUMNS)); + + final ConfigurationValueDAO configurationValueDAO = + Mockito.mock(ConfigurationValueDAO.class); + Mockito.when(configurationValueDAO.getOrderedTableValues(1L, 1L, 1L)) + .thenReturn(Result.of(Collections.emptyList())); + + final TableConverter tableConverter = new TableConverter(configurationAttributeDAO, configurationValueDAO); + tableConverter.init(createAttributeValueConverterService()); + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + + tableConverter.convertToXML(out, this.TABLE_ATTR, attr -> this.TABLE_VALUE); + + final String xmlString = new String(out.toByteArray()); + assertEquals( + "table", + xmlString); + + } + + @Test + public void testXMLCompositeTable() throws Exception { + + final ConfigurationAttributeDAO configurationAttributeDAO = + Mockito.mock(ConfigurationAttributeDAO.class); + Mockito.when(configurationAttributeDAO.allMatching(Mockito.any())) + .thenReturn(Result.of(this.COMPOSITE_TABLE_ENTRIES)); + + final ConfigurationValueDAO configurationValueDAO = + Mockito.mock(ConfigurationValueDAO.class); + Mockito.when(configurationValueDAO.getOrderedTableValues(1L, 1L, 1L)) + .thenReturn(Result.of(this.COMPOSITE_TABLE_VALUES)); + + final TableConverter tableConverter = new TableConverter(configurationAttributeDAO, configurationValueDAO); + tableConverter.init(createAttributeValueConverterService()); + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + + tableConverter.convertToXML(out, this.COMPOSITE_TABLE_ATTR, attr -> this.COMPOSITE_TABLE_VALUE); + + final String xmlString = new String(out.toByteArray()); + assertEquals( + "table" + + "" + + "attr1" + + "1" + + "attr2" + + "2" + + "", + xmlString); + + } + + @Test + public void testXMLCompositeTableEmpty() throws Exception { + + final ConfigurationAttributeDAO configurationAttributeDAO = + Mockito.mock(ConfigurationAttributeDAO.class); + Mockito.when(configurationAttributeDAO.allMatching(Mockito.any())) + .thenReturn(Result.of(this.COMPOSITE_TABLE_ENTRIES)); + + final ConfigurationValueDAO configurationValueDAO = + Mockito.mock(ConfigurationValueDAO.class); + Mockito.when(configurationValueDAO.getOrderedTableValues(1L, 1L, 1L)) + .thenReturn(Result.of(Collections.emptyList())); + + final TableConverter tableConverter = new TableConverter(configurationAttributeDAO, configurationValueDAO); + tableConverter.init(createAttributeValueConverterService()); + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + + tableConverter.convertToXML(out, this.COMPOSITE_TABLE_ATTR, attr -> this.COMPOSITE_TABLE_VALUE); + + final String xmlString = new String(out.toByteArray()); + assertEquals( + "", + xmlString); + + } + + @Test + public void testJSONNormalTable() throws Exception { + + final ConfigurationAttributeDAO configurationAttributeDAO = + Mockito.mock(ConfigurationAttributeDAO.class); + Mockito.when(configurationAttributeDAO.allMatching(Mockito.any())) + .thenReturn(Result.of(this.TABLE_COLUMNS)); + + final ConfigurationValueDAO configurationValueDAO = + Mockito.mock(ConfigurationValueDAO.class); + Mockito.when(configurationValueDAO.getOrderedTableValues(1L, 1L, 1L)) + .thenReturn(Result.of(this.TABLE_VALUES)); + + final TableConverter tableConverter = new TableConverter(configurationAttributeDAO, configurationValueDAO); + tableConverter.init(createAttributeValueConverterService()); + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + + tableConverter.convertToJSON(out, this.TABLE_ATTR, attr -> this.TABLE_VALUE); + + final String xmlString = new String(out.toByteArray()); + // expected : "table":[{"attr1":"1","attr2":"2"},{"attr1":"3","attr2":"4"}] + assertEquals( + "\"table\":[{\"attr1\":\"1\",\"attr2\":\"2\"},{\"attr1\":\"3\",\"attr2\":\"4\"}]", + xmlString); + + } + + @Test + public void testJSONNormalTableEmpty() throws Exception { + + final ConfigurationAttributeDAO configurationAttributeDAO = + Mockito.mock(ConfigurationAttributeDAO.class); + Mockito.when(configurationAttributeDAO.allMatching(Mockito.any())) + .thenReturn(Result.of(this.TABLE_COLUMNS)); + + final ConfigurationValueDAO configurationValueDAO = + Mockito.mock(ConfigurationValueDAO.class); + Mockito.when(configurationValueDAO.getOrderedTableValues(1L, 1L, 1L)) + .thenReturn(Result.of(Collections.emptyList())); + + final TableConverter tableConverter = new TableConverter(configurationAttributeDAO, configurationValueDAO); + tableConverter.init(createAttributeValueConverterService()); + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + + tableConverter.convertToJSON(out, this.TABLE_ATTR, attr -> this.TABLE_VALUE); + + final String xmlString = new String(out.toByteArray()); + // expected : "table":[] + assertEquals( + "\"table\":[]", + xmlString); + + } + + @Test + public void testJSONCompositeTable() throws Exception { + + final ConfigurationAttributeDAO configurationAttributeDAO = + Mockito.mock(ConfigurationAttributeDAO.class); + Mockito.when(configurationAttributeDAO.allMatching(Mockito.any())) + .thenReturn(Result.of(this.COMPOSITE_TABLE_ENTRIES)); + + final ConfigurationValueDAO configurationValueDAO = + Mockito.mock(ConfigurationValueDAO.class); + Mockito.when(configurationValueDAO.getOrderedTableValues(1L, 1L, 1L)) + .thenReturn(Result.of(this.COMPOSITE_TABLE_VALUES)); + + final TableConverter tableConverter = new TableConverter(configurationAttributeDAO, configurationValueDAO); + tableConverter.init(createAttributeValueConverterService()); + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + + tableConverter.convertToJSON(out, this.COMPOSITE_TABLE_ATTR, attr -> this.COMPOSITE_TABLE_VALUE); + + final String xmlString = new String(out.toByteArray()); + // expected : "table":{"attr1":"1","attr2":"2"} + assertEquals( + "\"table\":{\"attr1\":\"1\",\"attr2\":\"2\"}", + xmlString); + + } + + @Test + public void testJSONCompositeTableEmpty() throws Exception { + + final ConfigurationAttributeDAO configurationAttributeDAO = + Mockito.mock(ConfigurationAttributeDAO.class); + Mockito.when(configurationAttributeDAO.allMatching(Mockito.any())) + .thenReturn(Result.of(this.COMPOSITE_TABLE_ENTRIES)); + + final ConfigurationValueDAO configurationValueDAO = + Mockito.mock(ConfigurationValueDAO.class); + Mockito.when(configurationValueDAO.getOrderedTableValues(1L, 1L, 1L)) + .thenReturn(Result.of(Collections.emptyList())); + + final TableConverter tableConverter = new TableConverter(configurationAttributeDAO, configurationValueDAO); + tableConverter.init(createAttributeValueConverterService()); + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + + tableConverter.convertToJSON(out, this.COMPOSITE_TABLE_ATTR, attr -> this.COMPOSITE_TABLE_VALUE); + + final String xmlString = new String(out.toByteArray()); + // expected : + assertEquals( + "", + xmlString); + + } + + private AttributeValueConverterService createAttributeValueConverterService() { + final ClientCredentialService clientCredentialServiceMock = Mockito.mock(ClientCredentialService.class); + final List converter = new ArrayList<>(); + converter.add(new StringConverter(clientCredentialServiceMock)); + return new AttributeValueConverterServiceImpl(converter); + } + +}