SEBSERV-29 implementation of quiz data search from and LmsAPITemplate
This commit is contained in:
parent
c51016a548
commit
7b2f7228af
49 changed files with 1371 additions and 714 deletions
|
@ -14,6 +14,11 @@ import org.joda.time.format.DateTimeFormatter;
|
||||||
/** Global Constants used in SEB Server web-service as well as in web-gui component */
|
/** Global Constants used in SEB Server web-service as well as in web-gui component */
|
||||||
public final class Constants {
|
public final class Constants {
|
||||||
|
|
||||||
|
public static final long SECOND_IN_MILLIS = 1000;
|
||||||
|
public static final long MINUTE_IN_MILLIS = 60 * SECOND_IN_MILLIS;
|
||||||
|
public static final long HOUR_IN_MILLIS = 60 * MINUTE_IN_MILLIS;
|
||||||
|
public static final long DAY_IN_MILLIS = 24 * HOUR_IN_MILLIS;
|
||||||
|
|
||||||
public static final Character LIST_SEPARATOR_CHAR = ',';
|
public static final Character LIST_SEPARATOR_CHAR = ',';
|
||||||
public static final String LIST_SEPARATOR = ",";
|
public static final String LIST_SEPARATOR = ",";
|
||||||
public static final String EMPTY_NOTE = "--";
|
public static final String EMPTY_NOTE = "--";
|
||||||
|
|
|
@ -42,7 +42,9 @@ public final class API {
|
||||||
|
|
||||||
public static final String LMS_SETUP_ENDPOINT = "/lms_setup";
|
public static final String LMS_SETUP_ENDPOINT = "/lms_setup";
|
||||||
public static final String LMS_SETUP_TEST_PATH_SEGMENT = "/test";
|
public static final String LMS_SETUP_TEST_PATH_SEGMENT = "/test";
|
||||||
public static final String LMS_SETUP_TEST_ENDPOINT = LMS_SETUP_ENDPOINT + LMS_SETUP_TEST_PATH_SEGMENT;
|
public static final String LMS_SETUP_TEST_ENDPOINT = LMS_SETUP_ENDPOINT
|
||||||
|
+ LMS_SETUP_TEST_PATH_SEGMENT
|
||||||
|
+ MODEL_ID_VAR_PATH_SEGMENT;
|
||||||
|
|
||||||
public static final String USER_ACCOUNT_ENDPOINT = "/useraccount";
|
public static final String USER_ACCOUNT_ENDPOINT = "/useraccount";
|
||||||
|
|
||||||
|
|
|
@ -169,25 +169,30 @@ public class APIMessage implements Serializable {
|
||||||
|
|
||||||
private static final long serialVersionUID = 1453431210820677296L;
|
private static final long serialVersionUID = 1453431210820677296L;
|
||||||
|
|
||||||
private final APIMessage apiMessage;
|
private final Collection<APIMessage> apiMessages;
|
||||||
|
|
||||||
|
public APIMessageException(final Collection<APIMessage> apiMessages) {
|
||||||
|
super();
|
||||||
|
this.apiMessages = apiMessages;
|
||||||
|
}
|
||||||
|
|
||||||
public APIMessageException(final APIMessage apiMessage) {
|
public APIMessageException(final APIMessage apiMessage) {
|
||||||
super();
|
super();
|
||||||
this.apiMessage = apiMessage;
|
this.apiMessages = Arrays.asList(apiMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
public APIMessageException(final ErrorMessage errorMessage) {
|
public APIMessageException(final ErrorMessage errorMessage) {
|
||||||
super();
|
super();
|
||||||
this.apiMessage = errorMessage.of();
|
this.apiMessages = Arrays.asList(errorMessage.of());
|
||||||
}
|
}
|
||||||
|
|
||||||
public APIMessageException(final ErrorMessage errorMessage, final String detail, final String... attributes) {
|
public APIMessageException(final ErrorMessage errorMessage, final String detail, final String... attributes) {
|
||||||
super();
|
super();
|
||||||
this.apiMessage = errorMessage.of(detail, attributes);
|
this.apiMessages = Arrays.asList(errorMessage.of(detail, attributes));
|
||||||
}
|
}
|
||||||
|
|
||||||
public APIMessage getAPIMessage() {
|
public Collection<APIMessage> getAPIMessages() {
|
||||||
return this.apiMessage;
|
return this.apiMessages;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,8 +27,6 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.GrantEntity;
|
||||||
|
|
||||||
public final class Exam implements GrantEntity, Activatable {
|
public final class Exam implements GrantEntity, Activatable {
|
||||||
|
|
||||||
public static final String FILTER_ATTR_LMS_SETUP = "lms_setup";
|
|
||||||
public static final String FILTER_ATTR_NAME = "name_like";
|
|
||||||
public static final String FILTER_ATTR_STATUS = "status";
|
public static final String FILTER_ATTR_STATUS = "status";
|
||||||
public static final String FILTER_ATTR_TYPE = "type";
|
public static final String FILTER_ATTR_TYPE = "type";
|
||||||
public static final String FILTER_ATTR_FROM = "from";
|
public static final String FILTER_ATTR_FROM = "from";
|
||||||
|
|
|
@ -8,6 +8,8 @@
|
||||||
|
|
||||||
package ch.ethz.seb.sebserver.gbl.model.exam;
|
package ch.ethz.seb.sebserver.gbl.model.exam;
|
||||||
|
|
||||||
|
import java.util.Comparator;
|
||||||
|
|
||||||
import org.joda.time.DateTime;
|
import org.joda.time.DateTime;
|
||||||
import org.joda.time.DateTimeZone;
|
import org.joda.time.DateTimeZone;
|
||||||
import org.joda.time.LocalDateTime;
|
import org.joda.time.LocalDateTime;
|
||||||
|
@ -16,10 +18,12 @@ import com.fasterxml.jackson.annotation.JsonCreator;
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
import ch.ethz.seb.sebserver.gbl.Constants;
|
import ch.ethz.seb.sebserver.gbl.Constants;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.api.EntityType;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.model.Entity;
|
||||||
|
import ch.ethz.seb.sebserver.webservice.servicelayer.PaginationService.SortOrder;
|
||||||
|
|
||||||
public final class QuizData {
|
public final class QuizData implements Entity {
|
||||||
|
|
||||||
public static final String FILTER_ATTR_NAME = "name_like";
|
|
||||||
public static final String FILTER_ATTR_START_TIME = "start_timestamp";
|
public static final String FILTER_ATTR_START_TIME = "start_timestamp";
|
||||||
|
|
||||||
public static final String QUIZ_ATTR_ID = "quiz_id";
|
public static final String QUIZ_ATTR_ID = "quiz_id";
|
||||||
|
@ -84,10 +88,25 @@ public final class QuizData {
|
||||||
this.startURL = startURL;
|
this.startURL = startURL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getModelId() {
|
||||||
|
if (this.id == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return String.valueOf(this.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public EntityType entityType() {
|
||||||
|
return EntityType.EXAM;
|
||||||
|
}
|
||||||
|
|
||||||
public String geId() {
|
public String geId() {
|
||||||
return this.id;
|
return this.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public String getName() {
|
public String getName() {
|
||||||
return this.name;
|
return this.name;
|
||||||
}
|
}
|
||||||
|
@ -140,4 +159,62 @@ public final class QuizData {
|
||||||
+ ", endTime=" + this.endTime + ", startURL=" + this.startURL + "]";
|
+ ", endTime=" + this.endTime + ", startURL=" + this.startURL + "]";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Comparator<QuizData> getIdComparator(final boolean descending) {
|
||||||
|
return (qd1, qd2) -> ((qd1 == qd2)
|
||||||
|
? 0
|
||||||
|
: (qd1 == null || qd1.id == null)
|
||||||
|
? 1
|
||||||
|
: (qd2 == null || qd2.id == null)
|
||||||
|
? -1
|
||||||
|
: qd1.id.compareTo(qd2.id))
|
||||||
|
* ((descending) ? -1 : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Comparator<QuizData> getNameComparator(final boolean descending) {
|
||||||
|
return (qd1, qd2) -> ((qd1 == qd2)
|
||||||
|
? 0
|
||||||
|
: (qd1 == null || qd1.name == null)
|
||||||
|
? 1
|
||||||
|
: (qd2 == null || qd2.name == null)
|
||||||
|
? -1
|
||||||
|
: qd1.name.compareTo(qd2.name))
|
||||||
|
* ((descending) ? -1 : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Comparator<QuizData> getStartTimeComparator(final boolean descending) {
|
||||||
|
return (qd1, qd2) -> ((qd1 == qd2)
|
||||||
|
? 0
|
||||||
|
: (qd1 == null || qd1.startTime == null)
|
||||||
|
? 1
|
||||||
|
: (qd2 == null || qd2.startTime == null)
|
||||||
|
? -1
|
||||||
|
: qd1.startTime.compareTo(qd2.startTime))
|
||||||
|
* ((descending) ? -1 : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Comparator<QuizData> getEndTimeComparator(final boolean descending) {
|
||||||
|
return (qd1, qd2) -> ((qd1 == qd2)
|
||||||
|
? 0
|
||||||
|
: (qd1 == null || qd1.endTime == null)
|
||||||
|
? 1
|
||||||
|
: (qd2 == null || qd2.endTime == null)
|
||||||
|
? -1
|
||||||
|
: qd1.endTime.compareTo(qd2.endTime))
|
||||||
|
* ((descending) ? -1 : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Comparator<QuizData> getComparator(final String sort) {
|
||||||
|
final boolean descending = SortOrder.getSortOrder(sort) == SortOrder.DESCENDING;
|
||||||
|
final String sortParam = SortOrder.decode(sort);
|
||||||
|
if (QUIZ_ATTR_NAME.equals(sortParam)) {
|
||||||
|
return getNameComparator(descending);
|
||||||
|
} else if (QUIZ_ATTR_START_TIME.equals(sortParam)) {
|
||||||
|
return getStartTimeComparator(descending);
|
||||||
|
} else if (QUIZ_ATTR_END_TIME.equals(sortParam)) {
|
||||||
|
return getEndTimeComparator(descending);
|
||||||
|
}
|
||||||
|
|
||||||
|
return getIdComparator(descending);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,8 @@ package ch.ethz.seb.sebserver.gbl.model.institution;
|
||||||
import javax.validation.constraints.NotNull;
|
import javax.validation.constraints.NotNull;
|
||||||
import javax.validation.constraints.Size;
|
import javax.validation.constraints.Size;
|
||||||
|
|
||||||
|
import org.hibernate.validator.constraints.URL;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
@ -26,11 +28,11 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.GrantEntity;
|
||||||
|
|
||||||
public final class LmsSetup implements GrantEntity, Activatable {
|
public final class LmsSetup implements GrantEntity, Activatable {
|
||||||
|
|
||||||
|
public static final String FILTER_ATTR_LMS_SETUP = "lms_setup";
|
||||||
public static final String FILTER_ATTR_LMS_TYPE = "lms_type";
|
public static final String FILTER_ATTR_LMS_TYPE = "lms_type";
|
||||||
|
|
||||||
public enum LmsType {
|
public enum LmsType {
|
||||||
MOCKUP,
|
MOCKUP,
|
||||||
MOODLE,
|
|
||||||
OPEN_EDX
|
OPEN_EDX
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,14 +53,13 @@ public final class LmsSetup implements GrantEntity, Activatable {
|
||||||
public final LmsType lmsType;
|
public final LmsType lmsType;
|
||||||
|
|
||||||
@JsonProperty(LMS_SETUP.ATTR_LMS_CLIENTNAME)
|
@JsonProperty(LMS_SETUP.ATTR_LMS_CLIENTNAME)
|
||||||
@Size(min = 3, max = 255, message = "lmsSetup:lmsClientname:size:{min}:{max}:${validatedValue}")
|
|
||||||
public final String lmsAuthName;
|
public final String lmsAuthName;
|
||||||
|
|
||||||
@JsonProperty(LMS_SETUP.ATTR_LMS_CLIENTSECRET)
|
@JsonProperty(LMS_SETUP.ATTR_LMS_CLIENTSECRET)
|
||||||
@Size(min = 8, max = 255, message = "lmsSetup:lmsClientsecret:size:{min}:{max}:${validatedValue}")
|
|
||||||
public final String lmsAuthSecret;
|
public final String lmsAuthSecret;
|
||||||
|
|
||||||
@JsonProperty(LMS_SETUP.ATTR_LMS_URL)
|
@JsonProperty(LMS_SETUP.ATTR_LMS_URL)
|
||||||
|
@URL(message = "lmsSetup:lmsUrl:invalidURL")
|
||||||
public final String lmsApiUrl;
|
public final String lmsApiUrl;
|
||||||
|
|
||||||
@JsonProperty(LMS_SETUP.ATTR_LMS_REST_API_TOKEN)
|
@JsonProperty(LMS_SETUP.ATTR_LMS_REST_API_TOKEN)
|
||||||
|
|
|
@ -11,12 +11,14 @@ package ch.ethz.seb.sebserver.gbl.model.institution;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Set;
|
import java.util.List;
|
||||||
|
|
||||||
import javax.validation.constraints.NotNull;
|
import javax.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
|
||||||
import ch.ethz.seb.sebserver.gbl.util.Utils;
|
import ch.ethz.seb.sebserver.gbl.util.Utils;
|
||||||
|
|
||||||
public final class LmsSetupTestResult {
|
public final class LmsSetupTestResult {
|
||||||
|
@ -31,32 +33,37 @@ public final class LmsSetupTestResult {
|
||||||
public final Boolean okStatus;
|
public final Boolean okStatus;
|
||||||
|
|
||||||
@JsonProperty(ATTR_MISSING_ATTRIBUTE)
|
@JsonProperty(ATTR_MISSING_ATTRIBUTE)
|
||||||
public final Set<String> missingLMSSetupAttribute;
|
public final List<APIMessage> missingLMSSetupAttribute;
|
||||||
|
|
||||||
@JsonProperty(ATTR_MISSING_ATTRIBUTE)
|
@JsonProperty(ATTR_ERROR_TOKEN_REQUEST)
|
||||||
public final String tokenRequestError;
|
public final String tokenRequestError;
|
||||||
|
|
||||||
@JsonProperty(ATTR_MISSING_ATTRIBUTE)
|
@JsonProperty(ATTR_ERROR_QUIZ_REQUEST)
|
||||||
public final String quizRequestError;
|
public final String quizRequestError;
|
||||||
|
|
||||||
public LmsSetupTestResult(
|
public LmsSetupTestResult(
|
||||||
@JsonProperty(value = ATTR_OK_STATUS, required = true) final Boolean ok,
|
@JsonProperty(value = ATTR_OK_STATUS, required = true) final Boolean ok,
|
||||||
@JsonProperty(ATTR_MISSING_ATTRIBUTE) final Collection<String> missingLMSSetupAttribute,
|
@JsonProperty(ATTR_MISSING_ATTRIBUTE) final Collection<APIMessage> missingLMSSetupAttribute,
|
||||||
@JsonProperty(ATTR_MISSING_ATTRIBUTE) final String tokenRequestError,
|
@JsonProperty(ATTR_ERROR_TOKEN_REQUEST) final String tokenRequestError,
|
||||||
@JsonProperty(ATTR_MISSING_ATTRIBUTE) final String quizRequestError) {
|
@JsonProperty(ATTR_ERROR_QUIZ_REQUEST) final String quizRequestError) {
|
||||||
|
|
||||||
this.okStatus = ok;
|
this.okStatus = ok;
|
||||||
// TODO
|
// TODO
|
||||||
this.missingLMSSetupAttribute = Utils.immutableSetOf(missingLMSSetupAttribute);
|
this.missingLMSSetupAttribute = Utils.immutableListOf(missingLMSSetupAttribute);
|
||||||
this.tokenRequestError = tokenRequestError;
|
this.tokenRequestError = tokenRequestError;
|
||||||
this.quizRequestError = quizRequestError;
|
this.quizRequestError = quizRequestError;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
public boolean isOk() {
|
||||||
|
return this.okStatus != null && this.okStatus.booleanValue();
|
||||||
|
}
|
||||||
|
|
||||||
public Boolean getOkStatus() {
|
public Boolean getOkStatus() {
|
||||||
return this.okStatus;
|
return this.okStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Set<String> getMissingLMSSetupAttribute() {
|
public List<APIMessage> getMissingLMSSetupAttribute() {
|
||||||
return this.missingLMSSetupAttribute;
|
return this.missingLMSSetupAttribute;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,11 +86,11 @@ public final class LmsSetupTestResult {
|
||||||
return new LmsSetupTestResult(true, Collections.emptyList(), null, null);
|
return new LmsSetupTestResult(true, Collections.emptyList(), null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final LmsSetupTestResult ofMissingAttributes(final Collection<String> attrs) {
|
public static final LmsSetupTestResult ofMissingAttributes(final Collection<APIMessage> attrs) {
|
||||||
return new LmsSetupTestResult(false, attrs, null, null);
|
return new LmsSetupTestResult(false, attrs, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final LmsSetupTestResult ofMissingAttributes(final String... attrs) {
|
public static final LmsSetupTestResult ofMissingAttributes(final APIMessage... attrs) {
|
||||||
if (attrs == null) {
|
if (attrs == null) {
|
||||||
return new LmsSetupTestResult(false, Collections.emptyList(), null, null);
|
return new LmsSetupTestResult(false, Collections.emptyList(), null, null);
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,9 @@ import java.util.function.Function;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
/** A result of a computation that can either be the resulting value of the computation
|
/** A result of a computation that can either be the resulting value of the computation
|
||||||
* or an error if an exception/error has been thrown during the computation.
|
* or an error if an exception/error has been thrown during the computation.
|
||||||
*
|
*
|
||||||
|
@ -49,6 +52,8 @@ import java.util.stream.Stream;
|
||||||
* @param <T> The type of the result value */
|
* @param <T> The type of the result value */
|
||||||
public final class Result<T> {
|
public final class Result<T> {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(Result.class);
|
||||||
|
|
||||||
/** The resulting value. May be null if an error occurred */
|
/** The resulting value. May be null if an error occurred */
|
||||||
private final T value;
|
private final T value;
|
||||||
/** The error when happened otherwise null */
|
/** The error when happened otherwise null */
|
||||||
|
@ -270,6 +275,15 @@ public final class Result<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static <T> Stream<T> onErrorLogAndSkip(final Result<T> result) {
|
||||||
|
if (result.error != null) {
|
||||||
|
log.error("Unexpected error on result. Cause: ", result.error);
|
||||||
|
return Stream.empty();
|
||||||
|
} else {
|
||||||
|
return Stream.of(result.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
final int prime = 31;
|
final int prime = 31;
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET)
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ch.ethz.seb.sebserver.gbl.util;
|
||||||
|
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
public class SupplierWithCircuitBreaker<T> implements Supplier<Result<T>> {
|
||||||
|
|
||||||
|
private final Supplier<T> supplierThatCanFailOrBlock;
|
||||||
|
private final int maxFailingAttempts;
|
||||||
|
private final long maxBlockingTime;
|
||||||
|
|
||||||
|
private final T cached = null;
|
||||||
|
|
||||||
|
public SupplierWithCircuitBreaker(
|
||||||
|
final Supplier<T> supplierThatCanFailOrBlock,
|
||||||
|
final int maxFailingAttempts,
|
||||||
|
final long maxBlockingTime) {
|
||||||
|
|
||||||
|
this.supplierThatCanFailOrBlock = supplierThatCanFailOrBlock;
|
||||||
|
this.maxFailingAttempts = maxFailingAttempts;
|
||||||
|
this.maxBlockingTime = maxBlockingTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result<T> get() {
|
||||||
|
|
||||||
|
// TODO start an async task that calls the supplierThatCanFailOrBlock and returns a Future
|
||||||
|
// try to get the result periodically until maxBlockingTime
|
||||||
|
// if the supplier returns error, try for maxFailingAttempts
|
||||||
|
// if success cache and return the result
|
||||||
|
// if failed return the cached values
|
||||||
|
return Result.tryCatch(() -> this.supplierThatCanFailOrBlock.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -155,4 +155,10 @@ public final class Utils {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static final String formatHTMLLines(final String message) {
|
||||||
|
return (message != null)
|
||||||
|
? message.replace("\n", "<br/>")
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -174,7 +174,7 @@ public class InstitutionForm implements TemplateComposer {
|
||||||
.publishIf(() -> writeGrant && isReadonly && !institution.isActive())
|
.publishIf(() -> writeGrant && isReadonly && !institution.isActive())
|
||||||
|
|
||||||
.createAction(ActionDefinition.INSTITUTION_SAVE)
|
.createAction(ActionDefinition.INSTITUTION_SAVE)
|
||||||
.withExec(formHandle::postChanges)
|
.withExec(formHandle::processFormSave)
|
||||||
.publishIf(() -> !isReadonly)
|
.publishIf(() -> !isReadonly)
|
||||||
|
|
||||||
.createAction(ActionDefinition.INSTITUTION_CANCEL_MODIFY)
|
.createAction(ActionDefinition.INSTITUTION_CANCEL_MODIFY)
|
||||||
|
|
|
@ -10,6 +10,7 @@ package ch.ethz.seb.sebserver.gui.content;
|
||||||
|
|
||||||
import java.util.function.BooleanSupplier;
|
import java.util.function.BooleanSupplier;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.eclipse.swt.widgets.Composite;
|
import org.eclipse.swt.widgets.Composite;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
@ -21,9 +22,11 @@ import ch.ethz.seb.sebserver.gbl.model.Domain;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
|
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
|
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
|
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.user.UserInfo;
|
import ch.ethz.seb.sebserver.gbl.model.user.UserInfo;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.user.UserRole;
|
import ch.ethz.seb.sebserver.gbl.model.user.UserRole;
|
||||||
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
|
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||||
import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition;
|
import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition;
|
||||||
import ch.ethz.seb.sebserver.gui.form.FormBuilder;
|
import ch.ethz.seb.sebserver.gui.form.FormBuilder;
|
||||||
import ch.ethz.seb.sebserver.gui.form.FormHandle;
|
import ch.ethz.seb.sebserver.gui.form.FormHandle;
|
||||||
|
@ -31,6 +34,7 @@ import ch.ethz.seb.sebserver.gui.form.PageFormService;
|
||||||
import ch.ethz.seb.sebserver.gui.service.ResourceService;
|
import ch.ethz.seb.sebserver.gui.service.ResourceService;
|
||||||
import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
|
import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
|
||||||
import ch.ethz.seb.sebserver.gui.service.page.PageContext;
|
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.PageUtils;
|
import ch.ethz.seb.sebserver.gui.service.page.PageUtils;
|
||||||
import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer;
|
import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer;
|
||||||
import ch.ethz.seb.sebserver.gui.service.page.action.Action;
|
import ch.ethz.seb.sebserver.gui.service.page.action.Action;
|
||||||
|
@ -39,6 +43,7 @@ import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.institution.GetIn
|
||||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.lmssetup.GetLmsSetup;
|
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.lmssetup.GetLmsSetup;
|
||||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.lmssetup.NewLmsSetup;
|
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.lmssetup.NewLmsSetup;
|
||||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.lmssetup.SaveLmsSetup;
|
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.lmssetup.SaveLmsSetup;
|
||||||
|
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.lmssetup.TestLmsSetup;
|
||||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser;
|
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser;
|
||||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser.EntityGrantCheck;
|
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser.EntityGrantCheck;
|
||||||
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory;
|
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory;
|
||||||
|
@ -126,6 +131,9 @@ public class LmsSetupForm implements TemplateComposer {
|
||||||
.putStaticValueIf(isNotNew,
|
.putStaticValueIf(isNotNew,
|
||||||
Domain.LMS_SETUP.ATTR_INSTITUTION_ID,
|
Domain.LMS_SETUP.ATTR_INSTITUTION_ID,
|
||||||
String.valueOf(lmsSetup.getInstitutionId()))
|
String.valueOf(lmsSetup.getInstitutionId()))
|
||||||
|
.putStaticValueIf(isNotNew,
|
||||||
|
Domain.LMS_SETUP.ATTR_LMS_TYPE,
|
||||||
|
String.valueOf(lmsSetup.getLmsType()))
|
||||||
.addField(FormBuilder.singleSelection(
|
.addField(FormBuilder.singleSelection(
|
||||||
Domain.LMS_SETUP.ATTR_INSTITUTION_ID,
|
Domain.LMS_SETUP.ATTR_INSTITUTION_ID,
|
||||||
"sebserver.lmssetup.form.institution",
|
"sebserver.lmssetup.form.institution",
|
||||||
|
@ -143,22 +151,21 @@ public class LmsSetupForm implements TemplateComposer {
|
||||||
(lmsType != null) ? lmsType.name() : null,
|
(lmsType != null) ? lmsType.name() : null,
|
||||||
this.resourceService::lmsTypeResources)
|
this.resourceService::lmsTypeResources)
|
||||||
.readonlyIf(isNotNew))
|
.readonlyIf(isNotNew))
|
||||||
|
|
||||||
.addField(FormBuilder.text(
|
.addField(FormBuilder.text(
|
||||||
Domain.LMS_SETUP.ATTR_LMS_URL,
|
Domain.LMS_SETUP.ATTR_LMS_URL,
|
||||||
"sebserver.lmssetup.form.url",
|
"sebserver.lmssetup.form.url",
|
||||||
lmsSetup.getLmsApiUrl())
|
lmsSetup.getLmsApiUrl())
|
||||||
.withCondition(() -> isNotNew.getAsBoolean() && lmsType != LmsType.MOCKUP))
|
.withCondition(() -> isNotNew.getAsBoolean()))
|
||||||
.addField(FormBuilder.text(
|
.addField(FormBuilder.text(
|
||||||
Domain.LMS_SETUP.ATTR_LMS_CLIENTNAME,
|
Domain.LMS_SETUP.ATTR_LMS_CLIENTNAME,
|
||||||
"sebserver.lmssetup.form.clientname.lms",
|
"sebserver.lmssetup.form.clientname.lms",
|
||||||
lmsSetup.getLmsAuthName())
|
lmsSetup.getLmsAuthName())
|
||||||
.withCondition(() -> isNotNew.getAsBoolean() && lmsType != LmsType.MOCKUP))
|
.withCondition(() -> isNotNew.getAsBoolean()))
|
||||||
.addField(FormBuilder.text(
|
.addField(FormBuilder.text(
|
||||||
Domain.LMS_SETUP.ATTR_LMS_CLIENTSECRET,
|
Domain.LMS_SETUP.ATTR_LMS_CLIENTSECRET,
|
||||||
"sebserver.lmssetup.form.secret.lms")
|
"sebserver.lmssetup.form.secret.lms")
|
||||||
.asPasswordField()
|
.asPasswordField()
|
||||||
.withCondition(() -> isNotNew.getAsBoolean() && lmsType != LmsType.MOCKUP))
|
.withCondition(() -> isNotNew.getAsBoolean()))
|
||||||
|
|
||||||
.buildFor((entityKey == null)
|
.buildFor((entityKey == null)
|
||||||
? restService.getRestCall(NewLmsSetup.class)
|
? restService.getRestCall(NewLmsSetup.class)
|
||||||
|
@ -176,6 +183,11 @@ public class LmsSetupForm implements TemplateComposer {
|
||||||
.withEntityKey(entityKey)
|
.withEntityKey(entityKey)
|
||||||
.publishIf(() -> modifyGrant && readonly && istitutionActive)
|
.publishIf(() -> modifyGrant && readonly && istitutionActive)
|
||||||
|
|
||||||
|
.createAction(ActionDefinition.LMS_SETUP_TEST)
|
||||||
|
.withEntityKey(entityKey)
|
||||||
|
.withExec(action -> this.testLmsSetup(action, formHandle))
|
||||||
|
.publishIf(() -> modifyGrant && isNotNew.getAsBoolean() && istitutionActive)
|
||||||
|
|
||||||
.createAction(ActionDefinition.LMS_SETUP_DEACTIVATE)
|
.createAction(ActionDefinition.LMS_SETUP_DEACTIVATE)
|
||||||
.withEntityKey(entityKey)
|
.withEntityKey(entityKey)
|
||||||
.withExec(restService::activation)
|
.withExec(restService::activation)
|
||||||
|
@ -188,7 +200,7 @@ public class LmsSetupForm implements TemplateComposer {
|
||||||
.publishIf(() -> writeGrant && readonly && istitutionActive && !lmsSetup.isActive())
|
.publishIf(() -> writeGrant && readonly && istitutionActive && !lmsSetup.isActive())
|
||||||
|
|
||||||
.createAction(ActionDefinition.LMS_SETUP_SAVE)
|
.createAction(ActionDefinition.LMS_SETUP_SAVE)
|
||||||
.withExec(formHandle::postChanges)
|
.withExec(formHandle::processFormSave)
|
||||||
.publishIf(() -> !readonly)
|
.publishIf(() -> !readonly)
|
||||||
|
|
||||||
.createAction(ActionDefinition.LMS_SETUP_CANCEL_MODIFY)
|
.createAction(ActionDefinition.LMS_SETUP_CANCEL_MODIFY)
|
||||||
|
@ -199,4 +211,50 @@ public class LmsSetupForm implements TemplateComposer {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** LmsSetup test action implementation */
|
||||||
|
private Action testLmsSetup(final Action action, final FormHandle<LmsSetup> formHandle) {
|
||||||
|
// If we are in edit-mode we have to save the form before testing
|
||||||
|
if (!action.pageContext().isReadonly()) {
|
||||||
|
final Result<LmsSetup> postResult = formHandle.doAPIPost();
|
||||||
|
if (postResult.hasError()) {
|
||||||
|
formHandle.handleError(postResult.getError());
|
||||||
|
postResult.getOrThrow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the testing endpoint with the specified data to test
|
||||||
|
final EntityKey entityKey = action.getEntityKey();
|
||||||
|
final RestService restService = this.resourceService.getRestService();
|
||||||
|
final Result<LmsSetupTestResult> result = restService.getBuilder(TestLmsSetup.class)
|
||||||
|
.withURIVariable(API.PARAM_MODEL_ID, entityKey.getModelId())
|
||||||
|
.call();
|
||||||
|
|
||||||
|
// ... and handle the response
|
||||||
|
if (result.hasError()) {
|
||||||
|
if (formHandle.handleError(result.getError())) {
|
||||||
|
throw new PageMessageException(
|
||||||
|
new LocTextKey("sebserver.lmssetup.action.test.missingParameter"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final LmsSetupTestResult testResult = result.getOrThrow();
|
||||||
|
|
||||||
|
if (testResult.isOk()) {
|
||||||
|
action.pageContext().publishInfo(
|
||||||
|
new LocTextKey("sebserver.lmssetup.action.test.ok"));
|
||||||
|
|
||||||
|
return action;
|
||||||
|
} else if (StringUtils.isNoneBlank(testResult.tokenRequestError)) {
|
||||||
|
throw new PageMessageException(
|
||||||
|
new LocTextKey("sebserver.lmssetup.action.test.tokenRequestError",
|
||||||
|
testResult.tokenRequestError));
|
||||||
|
} else if (StringUtils.isNoneBlank(testResult.quizRequestError)) {
|
||||||
|
throw new PageMessageException(
|
||||||
|
new LocTextKey("sebserver.lmssetup.action.test.quizRequestError", testResult.quizRequestError));
|
||||||
|
} else {
|
||||||
|
throw new PageMessageException(
|
||||||
|
new LocTextKey("sebserver.lmssetup.action.test.unknownError", testResult));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -105,7 +105,7 @@ public class UserAccountChangePasswordForm implements TemplateComposer {
|
||||||
|
|
||||||
pageContext.createAction(ActionDefinition.USER_ACCOUNT_CHANGE_PASSOWRD_SAVE)
|
pageContext.createAction(ActionDefinition.USER_ACCOUNT_CHANGE_PASSOWRD_SAVE)
|
||||||
.withExec(action -> {
|
.withExec(action -> {
|
||||||
formHandle.postChanges(action);
|
formHandle.processFormSave(action);
|
||||||
if (ownAccount) {
|
if (ownAccount) {
|
||||||
// NOTE: in this case the user changed the password of the own account
|
// NOTE: in this case the user changed the password of the own account
|
||||||
// this should cause an logout with specified message that password change
|
// this should cause an logout with specified message that password change
|
||||||
|
|
|
@ -213,7 +213,7 @@ public class UserAccountForm implements TemplateComposer {
|
||||||
|
|
||||||
.createAction(ActionDefinition.USER_ACCOUNT_SAVE)
|
.createAction(ActionDefinition.USER_ACCOUNT_SAVE)
|
||||||
.withExec(action -> {
|
.withExec(action -> {
|
||||||
final Action postChanges = formHandle.postChanges(action);
|
final Action postChanges = formHandle.processFormSave(action);
|
||||||
if (ownAccount) {
|
if (ownAccount) {
|
||||||
currentUser.refresh();
|
currentUser.refresh();
|
||||||
pageContext.forwardToMainPage();
|
pageContext.forwardToMainPage();
|
||||||
|
|
|
@ -182,6 +182,11 @@ public enum ActionDefinition {
|
||||||
ImageIcon.EDIT,
|
ImageIcon.EDIT,
|
||||||
LmsSetupForm.class,
|
LmsSetupForm.class,
|
||||||
LMS_SETUP_VIEW_LIST, false),
|
LMS_SETUP_VIEW_LIST, false),
|
||||||
|
LMS_SETUP_TEST(
|
||||||
|
new LocTextKey("sebserver.lmssetup.action.test"),
|
||||||
|
ImageIcon.TEST,
|
||||||
|
LmsSetupForm.class,
|
||||||
|
LMS_SETUP_VIEW_LIST),
|
||||||
LMS_SETUP_CANCEL_MODIFY(
|
LMS_SETUP_CANCEL_MODIFY(
|
||||||
new LocTextKey("sebserver.overall.action.modify.cancel"),
|
new LocTextKey("sebserver.overall.action.modify.cancel"),
|
||||||
ImageIcon.CANCEL,
|
ImageIcon.CANCEL,
|
||||||
|
@ -213,13 +218,13 @@ public enum ActionDefinition {
|
||||||
public final Class<? extends RestCall<?>> restCallType;
|
public final Class<? extends RestCall<?>> restCallType;
|
||||||
public final ActionDefinition activityAlias;
|
public final ActionDefinition activityAlias;
|
||||||
public final String category;
|
public final String category;
|
||||||
public final boolean readonly;
|
public final Boolean readonly;
|
||||||
|
|
||||||
private ActionDefinition(
|
private ActionDefinition(
|
||||||
final LocTextKey title,
|
final LocTextKey title,
|
||||||
final Class<? extends TemplateComposer> contentPaneComposer) {
|
final Class<? extends TemplateComposer> contentPaneComposer) {
|
||||||
|
|
||||||
this(title, null, contentPaneComposer, ActionPane.class, null, null, null, true);
|
this(title, null, contentPaneComposer, ActionPane.class, null, null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ActionDefinition(
|
private ActionDefinition(
|
||||||
|
@ -227,7 +232,7 @@ public enum ActionDefinition {
|
||||||
final Class<? extends TemplateComposer> contentPaneComposer,
|
final Class<? extends TemplateComposer> contentPaneComposer,
|
||||||
final ActionDefinition activityAlias) {
|
final ActionDefinition activityAlias) {
|
||||||
|
|
||||||
this(title, null, contentPaneComposer, ActionPane.class, null, activityAlias, null, true);
|
this(title, null, contentPaneComposer, ActionPane.class, null, activityAlias, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ActionDefinition(
|
private ActionDefinition(
|
||||||
|
@ -236,7 +241,7 @@ public enum ActionDefinition {
|
||||||
final Class<? extends TemplateComposer> contentPaneComposer,
|
final Class<? extends TemplateComposer> contentPaneComposer,
|
||||||
final ActionDefinition activityAlias) {
|
final ActionDefinition activityAlias) {
|
||||||
|
|
||||||
this(title, icon, contentPaneComposer, ActionPane.class, null, activityAlias, null, true);
|
this(title, icon, contentPaneComposer, ActionPane.class, null, activityAlias, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ActionDefinition(
|
private ActionDefinition(
|
||||||
|
@ -246,7 +251,7 @@ public enum ActionDefinition {
|
||||||
final Class<? extends RestCall<?>> restCallType,
|
final Class<? extends RestCall<?>> restCallType,
|
||||||
final ActionDefinition activityAlias) {
|
final ActionDefinition activityAlias) {
|
||||||
|
|
||||||
this(title, icon, contentPaneComposer, ActionPane.class, restCallType, activityAlias, null, true);
|
this(title, icon, contentPaneComposer, ActionPane.class, restCallType, activityAlias, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ActionDefinition(
|
private ActionDefinition(
|
||||||
|
@ -267,7 +272,7 @@ public enum ActionDefinition {
|
||||||
final Class<? extends RestCall<?>> restCallType,
|
final Class<? extends RestCall<?>> restCallType,
|
||||||
final ActionDefinition activityAlias,
|
final ActionDefinition activityAlias,
|
||||||
final String category,
|
final String category,
|
||||||
final boolean readonly) {
|
final Boolean readonly) {
|
||||||
|
|
||||||
this.title = title;
|
this.title = title;
|
||||||
this.icon = icon;
|
this.icon = icon;
|
||||||
|
|
|
@ -162,6 +162,15 @@ public final class Form implements FormBinding {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean hasAnyError() {
|
||||||
|
return this.formFields.entrySet()
|
||||||
|
.stream()
|
||||||
|
.flatMap(entity -> entity.getValue().stream())
|
||||||
|
.filter(a -> a.hasError)
|
||||||
|
.findFirst()
|
||||||
|
.isPresent();
|
||||||
|
}
|
||||||
|
|
||||||
public void process(
|
public void process(
|
||||||
final Predicate<String> nameFilter,
|
final Predicate<String> nameFilter,
|
||||||
final Consumer<FormFieldAccessor> processor) {
|
final Consumer<FormFieldAccessor> processor) {
|
||||||
|
|
|
@ -13,9 +13,9 @@ import java.util.function.Consumer;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.Entity;
|
import ch.ethz.seb.sebserver.gbl.model.Entity;
|
||||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||||
import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition;
|
|
||||||
import ch.ethz.seb.sebserver.gui.form.Form.FormFieldAccessor;
|
import ch.ethz.seb.sebserver.gui.form.Form.FormFieldAccessor;
|
||||||
import ch.ethz.seb.sebserver.gui.service.i18n.I18nSupport;
|
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.LocTextKey;
|
||||||
|
@ -50,12 +50,24 @@ public class FormHandle<T extends Entity> {
|
||||||
this.i18nSupport = i18nSupport;
|
this.i18nSupport = i18nSupport;
|
||||||
}
|
}
|
||||||
|
|
||||||
public final Action postChanges(final Action action) {
|
/** Process an API post request to send and save the form field values
|
||||||
return doAPIPost(action.definition)
|
* to the webservice and publishes a page event to return to read-only-view
|
||||||
.getOrThrow();
|
* to indicate that the data was successfully saved or process an validation
|
||||||
|
* error indication if there are some validation errors.
|
||||||
|
*
|
||||||
|
* @param action the save action context
|
||||||
|
* @return the new Action context for read-only-view */
|
||||||
|
public final Action processFormSave(final Action action) {
|
||||||
|
return handleFormPost(doAPIPost(), action);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Result<Action> doAPIPost(final ActionDefinition actionDefinition) {
|
/** process a form post by first resetting all field validation errors (if there are some)
|
||||||
|
* then collecting all input data from the form by form-binding to a either a JSON string in
|
||||||
|
* HTTP PUT case or to an form-URL-encoded string on HTTP POST case. And PUT or POST the data
|
||||||
|
* to the webservice by using the defined RestCall and return the response result of the RestCall.
|
||||||
|
*
|
||||||
|
* @return the response result of the post (or put) RestCall */
|
||||||
|
public Result<T> doAPIPost() {
|
||||||
this.form.process(
|
this.form.process(
|
||||||
name -> true,
|
name -> true,
|
||||||
fieldAccessor -> fieldAccessor.resetError());
|
fieldAccessor -> fieldAccessor.resetError());
|
||||||
|
@ -63,34 +75,52 @@ public class FormHandle<T extends Entity> {
|
||||||
return this.post
|
return this.post
|
||||||
.newBuilder()
|
.newBuilder()
|
||||||
.withFormBinding(this.form)
|
.withFormBinding(this.form)
|
||||||
.call()
|
.call();
|
||||||
.map(result -> {
|
|
||||||
final Action action = this.pageContext.createAction(actionDefinition)
|
|
||||||
.withAttribute(AttributeKeys.READ_ONLY, "true")
|
|
||||||
.withEntityKey(result.getEntityKey());
|
|
||||||
this.pageContext.publishPageEvent(new ActionEvent(action, false));
|
|
||||||
return action;
|
|
||||||
})
|
|
||||||
.onErrorDo(this::handleError)
|
|
||||||
//.map(this.postPostHandle)
|
|
||||||
;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleError(final Throwable error) {
|
/** Uses the result of a form post to either create and publish a new Action to
|
||||||
|
* go to the read-only-view of the specified form to indicate a successful form post
|
||||||
|
* or stay within the edit-mode of the form and indicate errors or field validation messages
|
||||||
|
* to the user on error case.
|
||||||
|
*
|
||||||
|
* @param postResult The form post result
|
||||||
|
* @param action the action that was applied with the form post
|
||||||
|
* @return the new Action that was used to stay on page or go the read-only-view of the form */
|
||||||
|
public Action handleFormPost(final Result<T> postResult, final Action action) {
|
||||||
|
return postResult
|
||||||
|
.map(result -> {
|
||||||
|
final Action resultAction = action.createNew()
|
||||||
|
.withAttribute(AttributeKeys.READ_ONLY, "true")
|
||||||
|
.withEntityKey(result.getEntityKey());
|
||||||
|
action.pageContext().publishPageEvent(new ActionEvent(resultAction, false));
|
||||||
|
return resultAction;
|
||||||
|
})
|
||||||
|
.onErrorDo(this::handleError)
|
||||||
|
.getOrThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean handleError(final Throwable error) {
|
||||||
if (error instanceof RestCallError) {
|
if (error instanceof RestCallError) {
|
||||||
((RestCallError) error)
|
((RestCallError) error)
|
||||||
.getErrorMessages()
|
.getErrorMessages()
|
||||||
.stream()
|
.stream()
|
||||||
|
.filter(APIMessage.ErrorMessage.FIELD_VALIDATION::isOf)
|
||||||
.map(FieldValidationError::new)
|
.map(FieldValidationError::new)
|
||||||
.forEach(fve -> this.form.process(
|
.forEach(fve -> this.form.process(
|
||||||
name -> name.equals(fve.fieldName),
|
name -> name.equals(fve.fieldName),
|
||||||
fieldAccessor -> showValidationError(fieldAccessor, fve)));
|
fieldAccessor -> showValidationError(fieldAccessor, fve)));
|
||||||
|
return true;
|
||||||
} else {
|
} else {
|
||||||
log.error("Unexpected error while trying to post form: ", error);
|
log.error("Unexpected error while trying to post form: ", error);
|
||||||
this.pageContext.notifyError(error);
|
this.pageContext.notifyError(error);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean hasAnyError() {
|
||||||
|
return this.form.hasAnyError();
|
||||||
|
}
|
||||||
|
|
||||||
private final void showValidationError(
|
private final void showValidationError(
|
||||||
final FormFieldAccessor fieldAccessor,
|
final FormFieldAccessor fieldAccessor,
|
||||||
final FieldValidationError valError) {
|
final FieldValidationError valError) {
|
||||||
|
|
|
@ -206,6 +206,14 @@ public interface PageContext {
|
||||||
* @param message the localized text key of the message */
|
* @param message the localized text key of the message */
|
||||||
void publishPageMessage(LocTextKey title, LocTextKey message);
|
void publishPageMessage(LocTextKey title, LocTextKey message);
|
||||||
|
|
||||||
|
/** Publish an information message to the user with the given localized message.
|
||||||
|
* The message text can also be HTML text as far as RWT supports it
|
||||||
|
*
|
||||||
|
* @param message the localized text key of the message */
|
||||||
|
default void publishInfo(final LocTextKey message) {
|
||||||
|
publishPageMessage(new LocTextKey("sebserver.page.message"), message);
|
||||||
|
}
|
||||||
|
|
||||||
/** Publish and shows a formatted PageMessageException to the user.
|
/** Publish and shows a formatted PageMessageException to the user.
|
||||||
*
|
*
|
||||||
* @param pme the PageMessageException */
|
* @param pme the PageMessageException */
|
||||||
|
|
|
@ -48,9 +48,12 @@ public final class Action implements Runnable {
|
||||||
|
|
||||||
this.definition = definition;
|
this.definition = definition;
|
||||||
this.originalPageContext = pageContext;
|
this.originalPageContext = pageContext;
|
||||||
|
final String readonly = pageContext.getAttribute(AttributeKeys.READ_ONLY, "true");
|
||||||
this.pageContext = pageContext.withAttribute(
|
this.pageContext = pageContext.withAttribute(
|
||||||
AttributeKeys.READ_ONLY,
|
AttributeKeys.READ_ONLY,
|
||||||
String.valueOf(definition.readonly));
|
definition.readonly != null
|
||||||
|
? String.valueOf(definition.readonly)
|
||||||
|
: readonly);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -89,6 +92,10 @@ public final class Action implements Runnable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Action createNew() {
|
||||||
|
return this.pageContext.createAction(this.definition);
|
||||||
|
}
|
||||||
|
|
||||||
public Action withExec(final Function<Action, Action> exec) {
|
public Action withExec(final Function<Action, Action> exec) {
|
||||||
this.exec = exec;
|
this.exec = exec;
|
||||||
return this;
|
return this;
|
||||||
|
|
|
@ -313,7 +313,7 @@ public class PageContextImpl implements PageContext {
|
||||||
final MessageBox messageBox = new Message(
|
final MessageBox messageBox = new Message(
|
||||||
getShell(),
|
getShell(),
|
||||||
this.i18nSupport.getText("sebserver.error.unexpected"),
|
this.i18nSupport.getText("sebserver.error.unexpected"),
|
||||||
error.toString(),
|
Utils.formatHTMLLines(errorMessage),
|
||||||
SWT.ERROR);
|
SWT.ERROR);
|
||||||
messageBox.open(null);
|
messageBox.open(null);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET)
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ch.ethz.seb.sebserver.gui.service.remote.webservice.api.lmssetup;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
|
||||||
|
import ch.ethz.seb.sebserver.gbl.api.API;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.api.EntityType;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
|
||||||
|
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
|
||||||
|
|
||||||
|
@Lazy
|
||||||
|
@Component
|
||||||
|
@GuiProfile
|
||||||
|
public class TestLmsSetup extends RestCall<LmsSetupTestResult> {
|
||||||
|
|
||||||
|
protected TestLmsSetup() {
|
||||||
|
super(new TypeKey<>(
|
||||||
|
CallType.UNDEFINED,
|
||||||
|
EntityType.LMS_SETUP,
|
||||||
|
new TypeReference<LmsSetupTestResult>() {
|
||||||
|
}),
|
||||||
|
HttpMethod.GET,
|
||||||
|
MediaType.APPLICATION_FORM_URLENCODED,
|
||||||
|
API.LMS_SETUP_TEST_ENDPOINT);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -61,6 +61,7 @@ public class WidgetFactory {
|
||||||
MAXIMIZE("maximize.png"),
|
MAXIMIZE("maximize.png"),
|
||||||
MINIMIZE("minimize.png"),
|
MINIMIZE("minimize.png"),
|
||||||
EDIT("edit.png"),
|
EDIT("edit.png"),
|
||||||
|
TEST("test.png"),
|
||||||
CANCEL("cancel.png"),
|
CANCEL("cancel.png"),
|
||||||
CANCEL_EDIT("cancelEdit.png"),
|
CANCEL_EDIT("cancelEdit.png"),
|
||||||
SHOW("show.png"),
|
SHOW("show.png"),
|
||||||
|
|
|
@ -23,16 +23,29 @@ import org.springframework.jdbc.datasource.DataSourceTransactionManager;
|
||||||
|
|
||||||
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
||||||
|
|
||||||
|
/** The MyBatis - Spring configuration
|
||||||
|
*
|
||||||
|
* All mapper- and model-classes in the specified sub-packages
|
||||||
|
* are auto-generated from DB schema by an external generator
|
||||||
|
*
|
||||||
|
* MyBatis is used on the lowest data - layer as an OR-Mapper with great flexibility and a good
|
||||||
|
* SQL builder interface.
|
||||||
|
*
|
||||||
|
* The Datasource is auto-configured by Spring and depends on the Spring property configuration so far */
|
||||||
@Configuration
|
@Configuration
|
||||||
@MapperScan(basePackages = "ch.ethz.seb.sebserver.webservice.datalayer.batis")
|
@MapperScan(basePackages = "ch.ethz.seb.sebserver.webservice.datalayer.batis")
|
||||||
@WebServiceProfile
|
@WebServiceProfile
|
||||||
@Import(DataSourceAutoConfiguration.class)
|
@Import(DataSourceAutoConfiguration.class)
|
||||||
public class BatisConfig {
|
public class BatisConfig {
|
||||||
|
|
||||||
|
/** Name of the transaction manager bean for MyBatis based Spring controlled transactions */
|
||||||
public static final String TRANSACTION_MANAGER = "transactionManager";
|
public static final String TRANSACTION_MANAGER = "transactionManager";
|
||||||
|
/** Name of the sql session template bean of MyBatis */
|
||||||
public static final String SQL_SESSION_TEMPLATE = "sqlSessionTemplate";
|
public static final String SQL_SESSION_TEMPLATE = "sqlSessionTemplate";
|
||||||
|
/** Name of the sql session factory bean of MyBatis */
|
||||||
public static final String SQL_SESSION_FACTORY = "sqlSessionFactory";
|
public static final String SQL_SESSION_FACTORY = "sqlSessionFactory";
|
||||||
|
|
||||||
|
/** Transaction manager bean for MyBatis based Spring controlled transactions */
|
||||||
@Lazy
|
@Lazy
|
||||||
@Bean(name = SQL_SESSION_FACTORY)
|
@Bean(name = SQL_SESSION_FACTORY)
|
||||||
public SqlSessionFactory sqlSessionFactory(final DataSource dataSource) throws Exception {
|
public SqlSessionFactory sqlSessionFactory(final DataSource dataSource) throws Exception {
|
||||||
|
@ -41,6 +54,7 @@ public class BatisConfig {
|
||||||
return factoryBean.getObject();
|
return factoryBean.getObject();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** SQL session template bean of MyBatis */
|
||||||
@Lazy
|
@Lazy
|
||||||
@Bean(name = SQL_SESSION_TEMPLATE)
|
@Bean(name = SQL_SESSION_TEMPLATE)
|
||||||
public SqlSessionTemplate sqlSessionTemplate(final DataSource dataSource) throws Exception {
|
public SqlSessionTemplate sqlSessionTemplate(final DataSource dataSource) throws Exception {
|
||||||
|
@ -48,6 +62,7 @@ public class BatisConfig {
|
||||||
return sqlSessionTemplate;
|
return sqlSessionTemplate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** SQL session factory bean of MyBatis */
|
||||||
@Lazy
|
@Lazy
|
||||||
@Bean(name = TRANSACTION_MANAGER)
|
@Bean(name = TRANSACTION_MANAGER)
|
||||||
public DataSourceTransactionManager transactionManager(final DataSource dataSource) {
|
public DataSourceTransactionManager transactionManager(final DataSource dataSource) {
|
||||||
|
|
|
@ -229,6 +229,16 @@ public interface AuthorizationService {
|
||||||
return check(PrivilegeType.WRITE, grantEntity);
|
return check(PrivilegeType.WRITE, grantEntity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Checks if the current user has role based view access to a specified user account.
|
||||||
|
*
|
||||||
|
* If user account has UserRole.SEB_SERVER_ADMIN this always gives true
|
||||||
|
* If user account has UserRole.INSTITUTIONAL_ADMIN this is true if the given user account has
|
||||||
|
* not the UserRole.SEB_SERVER_ADMIN (institutional administrators should not see SEB Server administrators)
|
||||||
|
* If the current user is the same as the given user account this is always true no matter if there are any
|
||||||
|
* user-account based privileges (every user shall see its own account)
|
||||||
|
*
|
||||||
|
* @param userAccount the user account the check role based view access
|
||||||
|
* @return true if the current user has role based view access to a specified user account */
|
||||||
default boolean hasRoleBasedUserAccountViewGrant(final UserInfo userAccount) {
|
default boolean hasRoleBasedUserAccountViewGrant(final UserInfo userAccount) {
|
||||||
final EnumSet<UserRole> userRolesOfUserAccount = userAccount.getUserRoles();
|
final EnumSet<UserRole> userRolesOfUserAccount = userAccount.getUserRoles();
|
||||||
final SEBServerUser currentUser = getUserService().getCurrentUser();
|
final SEBServerUser currentUser = getUserService().getCurrentUser();
|
||||||
|
|
|
@ -50,6 +50,10 @@ public interface UserService {
|
||||||
* @return an overall super user with all rights */
|
* @return an overall super user with all rights */
|
||||||
SEBServerUser getSuperUser();
|
SEBServerUser getSuperUser();
|
||||||
|
|
||||||
|
/** Binds the current users institution identifier as default value to a
|
||||||
|
*
|
||||||
|
* @RequestParam of type API.PARAM_INSTITUTION_ID if needed. See EntityController class for example
|
||||||
|
* @param binder Springs WebDataBinder is injected on controller side */
|
||||||
void addUsersInstitutionDefaultPropertySupport(final WebDataBinder binder);
|
void addUsersInstitutionDefaultPropertySupport(final WebDataBinder binder);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,22 +16,30 @@ import java.util.LinkedHashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import ch.ethz.seb.sebserver.gbl.api.EntityType;
|
|
||||||
import ch.ethz.seb.sebserver.gbl.api.API.BulkActionType;
|
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.EntityKey;
|
||||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||||
import ch.ethz.seb.sebserver.gbl.util.Utils;
|
import ch.ethz.seb.sebserver.gbl.util.Utils;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO.ActivityType;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO.ActivityType;
|
||||||
|
|
||||||
|
/** Defines a bulk action with its type, source entities (and source-type) and dependent entities.
|
||||||
|
* A BulkAction acts as a collector for entities (keys) that depends on the Bulk Action during the
|
||||||
|
* dependency collection phase.
|
||||||
|
* A BulkAction also acts as a result collector during the bulk-action process phase. */
|
||||||
public final class BulkAction {
|
public final class BulkAction {
|
||||||
|
|
||||||
|
/** Defines the type of the BulkAction */
|
||||||
public final BulkActionType type;
|
public final BulkActionType type;
|
||||||
|
/** Defines the EntityType of the source entities of the BulkAction */
|
||||||
public final EntityType sourceType;
|
public final EntityType sourceType;
|
||||||
|
/** A Set of EntityKey defining all source-entities of the BulkAction */
|
||||||
public final Set<EntityKey> sources;
|
public final Set<EntityKey> sources;
|
||||||
|
/** A Set of EntityKey containing collected depending entities during dependency collection and processing phase */
|
||||||
final Set<EntityKey> dependencies;
|
final Set<EntityKey> dependencies;
|
||||||
|
/** A Set of EntityKey containing collected bulk action processing results during processing phase */
|
||||||
final Set<Result<EntityKey>> result;
|
final Set<Result<EntityKey>> result;
|
||||||
|
/** Indicates if this BulkAction has already been processed and is not valid anymore */
|
||||||
boolean alreadyProcessed = false;
|
boolean alreadyProcessed = false;
|
||||||
|
|
||||||
public BulkAction(
|
public BulkAction(
|
||||||
|
|
|
@ -8,176 +8,15 @@
|
||||||
|
|
||||||
package ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction;
|
package ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction;
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import ch.ethz.seb.sebserver.gbl.api.EntityType;
|
|
||||||
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
|
|
||||||
import ch.ethz.seb.sebserver.gbl.model.EntityProcessingReport;
|
import ch.ethz.seb.sebserver.gbl.model.EntityProcessingReport;
|
||||||
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
|
||||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO;
|
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO.ActivityType;
|
|
||||||
|
|
||||||
@Service
|
public interface BulkActionService {
|
||||||
@WebServiceProfile
|
|
||||||
public class BulkActionService {
|
|
||||||
|
|
||||||
private final Map<EntityType, BulkActionSupportDAO<?>> supporter;
|
void collectDependencies(BulkAction action);
|
||||||
private final UserActivityLogDAO userActivityLogDAO;
|
|
||||||
|
|
||||||
public BulkActionService(
|
Result<BulkAction> doBulkAction(BulkAction action);
|
||||||
final Collection<BulkActionSupportDAO<?>> supporter,
|
|
||||||
final UserActivityLogDAO userActivityLogDAO) {
|
|
||||||
|
|
||||||
this.supporter = new HashMap<>();
|
Result<EntityProcessingReport> createReport(BulkAction action);
|
||||||
for (final BulkActionSupportDAO<?> support : supporter) {
|
|
||||||
this.supporter.put(support.entityType(), support);
|
|
||||||
}
|
|
||||||
this.userActivityLogDAO = userActivityLogDAO;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void collectDependencies(final BulkAction action) {
|
}
|
||||||
checkProcessing(action);
|
|
||||||
for (final BulkActionSupportDAO<?> sup : this.supporter.values()) {
|
|
||||||
action.dependencies.addAll(sup.getDependencies(action));
|
|
||||||
}
|
|
||||||
action.alreadyProcessed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Result<BulkAction> doBulkAction(final BulkAction action) {
|
|
||||||
return Result.tryCatch(() -> {
|
|
||||||
|
|
||||||
checkProcessing(action);
|
|
||||||
|
|
||||||
final BulkActionSupportDAO<?> supportForSource = this.supporter
|
|
||||||
.get(action.sourceType);
|
|
||||||
if (supportForSource == null) {
|
|
||||||
action.alreadyProcessed = true;
|
|
||||||
throw new IllegalArgumentException("No bulk action support for: " + action);
|
|
||||||
}
|
|
||||||
|
|
||||||
collectDependencies(action);
|
|
||||||
|
|
||||||
if (!action.dependencies.isEmpty()) {
|
|
||||||
// process dependencies first...
|
|
||||||
final List<BulkActionSupportDAO<?>> dependancySupporter =
|
|
||||||
getDependancySupporter(action);
|
|
||||||
|
|
||||||
for (final BulkActionSupportDAO<?> support : dependancySupporter) {
|
|
||||||
action.result.addAll(support.processBulkAction(action));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
action.result.addAll(supportForSource.processBulkAction(action));
|
|
||||||
|
|
||||||
processUserActivityLog(action);
|
|
||||||
action.alreadyProcessed = true;
|
|
||||||
return action;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public Result<EntityProcessingReport> createReport(final BulkAction action) {
|
|
||||||
if (!action.alreadyProcessed) {
|
|
||||||
return doBulkAction(action)
|
|
||||||
.flatMap(this::createFullReport);
|
|
||||||
} else {
|
|
||||||
return createFullReport(action);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Result<EntityProcessingReport> createFullReport(final BulkAction action) {
|
|
||||||
return Result.tryCatch(() -> {
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
return new EntityProcessingReport(
|
|
||||||
action.sources,
|
|
||||||
Collections.emptyList(),
|
|
||||||
Collections.emptyList());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void processUserActivityLog(final BulkAction action) {
|
|
||||||
final ActivityType activityType = action.getActivityType();
|
|
||||||
|
|
||||||
if (activityType == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (final EntityKey key : action.dependencies) {
|
|
||||||
this.userActivityLogDAO.log(
|
|
||||||
activityType,
|
|
||||||
key.entityType,
|
|
||||||
key.modelId,
|
|
||||||
"bulk action dependency");
|
|
||||||
}
|
|
||||||
|
|
||||||
for (final EntityKey key : action.sources) {
|
|
||||||
this.userActivityLogDAO.log(
|
|
||||||
activityType,
|
|
||||||
key.entityType,
|
|
||||||
key.modelId,
|
|
||||||
"bulk action source");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<BulkActionSupportDAO<?>> getDependancySupporter(final BulkAction action) {
|
|
||||||
switch (action.type) {
|
|
||||||
case ACTIVATE:
|
|
||||||
case DEACTIVATE:
|
|
||||||
case HARD_DELETE: {
|
|
||||||
final List<BulkActionSupportDAO<?>> dependantSupporterInHierarchicalOrder =
|
|
||||||
getDependantSupporterInHierarchicalOrder(action);
|
|
||||||
Collections.reverse(dependantSupporterInHierarchicalOrder);
|
|
||||||
return dependantSupporterInHierarchicalOrder
|
|
||||||
.stream()
|
|
||||||
.filter(v -> v != null)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return getDependantSupporterInHierarchicalOrder(action);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<BulkActionSupportDAO<?>> getDependantSupporterInHierarchicalOrder(final BulkAction action) {
|
|
||||||
switch (action.sourceType) {
|
|
||||||
case INSTITUTION:
|
|
||||||
return Arrays.asList(
|
|
||||||
this.supporter.get(EntityType.LMS_SETUP),
|
|
||||||
this.supporter.get(EntityType.USER),
|
|
||||||
this.supporter.get(EntityType.EXAM),
|
|
||||||
this.supporter.get(EntityType.INDICATOR),
|
|
||||||
this.supporter.get(EntityType.CLIENT_CONNECTION),
|
|
||||||
this.supporter.get(EntityType.CONFIGURATION_NODE));
|
|
||||||
case USER:
|
|
||||||
return Arrays.asList(
|
|
||||||
this.supporter.get(EntityType.EXAM),
|
|
||||||
this.supporter.get(EntityType.INDICATOR),
|
|
||||||
this.supporter.get(EntityType.CLIENT_CONNECTION),
|
|
||||||
this.supporter.get(EntityType.CONFIGURATION_NODE));
|
|
||||||
case LMS_SETUP:
|
|
||||||
case EXAM:
|
|
||||||
case CONFIGURATION:
|
|
||||||
return Arrays.asList(
|
|
||||||
this.supporter.get(EntityType.EXAM),
|
|
||||||
this.supporter.get(EntityType.INDICATOR),
|
|
||||||
this.supporter.get(EntityType.CLIENT_CONNECTION));
|
|
||||||
default:
|
|
||||||
return Collections.emptyList();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkProcessing(final BulkAction action) {
|
|
||||||
if (action.alreadyProcessed) {
|
|
||||||
throw new IllegalStateException("Given BulkAction has already been processed. Use a new one");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -0,0 +1,186 @@
|
||||||
|
/*
|
||||||
|
* 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.bulkaction;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import ch.ethz.seb.sebserver.gbl.api.EntityType;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.model.EntityProcessingReport;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||||
|
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO;
|
||||||
|
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO.ActivityType;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@WebServiceProfile
|
||||||
|
public class BulkActionServiceImpl implements BulkActionService {
|
||||||
|
|
||||||
|
private final Map<EntityType, BulkActionSupportDAO<?>> supporter;
|
||||||
|
private final UserActivityLogDAO userActivityLogDAO;
|
||||||
|
|
||||||
|
public BulkActionServiceImpl(
|
||||||
|
final Collection<BulkActionSupportDAO<?>> supporter,
|
||||||
|
final UserActivityLogDAO userActivityLogDAO) {
|
||||||
|
|
||||||
|
this.supporter = new HashMap<>();
|
||||||
|
for (final BulkActionSupportDAO<?> support : supporter) {
|
||||||
|
this.supporter.put(support.entityType(), support);
|
||||||
|
}
|
||||||
|
this.userActivityLogDAO = userActivityLogDAO;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void collectDependencies(final BulkAction action) {
|
||||||
|
checkProcessing(action);
|
||||||
|
for (final BulkActionSupportDAO<?> sup : this.supporter.values()) {
|
||||||
|
action.dependencies.addAll(sup.getDependencies(action));
|
||||||
|
}
|
||||||
|
action.alreadyProcessed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result<BulkAction> doBulkAction(final BulkAction action) {
|
||||||
|
return Result.tryCatch(() -> {
|
||||||
|
|
||||||
|
checkProcessing(action);
|
||||||
|
|
||||||
|
final BulkActionSupportDAO<?> supportForSource = this.supporter
|
||||||
|
.get(action.sourceType);
|
||||||
|
if (supportForSource == null) {
|
||||||
|
action.alreadyProcessed = true;
|
||||||
|
throw new IllegalArgumentException("No bulk action support for: " + action);
|
||||||
|
}
|
||||||
|
|
||||||
|
collectDependencies(action);
|
||||||
|
|
||||||
|
if (!action.dependencies.isEmpty()) {
|
||||||
|
// process dependencies first...
|
||||||
|
final List<BulkActionSupportDAO<?>> dependancySupporter =
|
||||||
|
getDependancySupporter(action);
|
||||||
|
|
||||||
|
for (final BulkActionSupportDAO<?> support : dependancySupporter) {
|
||||||
|
action.result.addAll(support.processBulkAction(action));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
action.result.addAll(supportForSource.processBulkAction(action));
|
||||||
|
|
||||||
|
processUserActivityLog(action);
|
||||||
|
action.alreadyProcessed = true;
|
||||||
|
return action;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result<EntityProcessingReport> createReport(final BulkAction action) {
|
||||||
|
if (!action.alreadyProcessed) {
|
||||||
|
return doBulkAction(action)
|
||||||
|
.flatMap(this::createFullReport);
|
||||||
|
} else {
|
||||||
|
return createFullReport(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Result<EntityProcessingReport> createFullReport(final BulkAction action) {
|
||||||
|
return Result.tryCatch(() -> {
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
return new EntityProcessingReport(
|
||||||
|
action.sources,
|
||||||
|
Collections.emptyList(),
|
||||||
|
Collections.emptyList());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processUserActivityLog(final BulkAction action) {
|
||||||
|
final ActivityType activityType = action.getActivityType();
|
||||||
|
|
||||||
|
if (activityType == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final EntityKey key : action.dependencies) {
|
||||||
|
this.userActivityLogDAO.log(
|
||||||
|
activityType,
|
||||||
|
key.entityType,
|
||||||
|
key.modelId,
|
||||||
|
"bulk action dependency");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final EntityKey key : action.sources) {
|
||||||
|
this.userActivityLogDAO.log(
|
||||||
|
activityType,
|
||||||
|
key.entityType,
|
||||||
|
key.modelId,
|
||||||
|
"bulk action source");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<BulkActionSupportDAO<?>> getDependancySupporter(final BulkAction action) {
|
||||||
|
switch (action.type) {
|
||||||
|
case ACTIVATE:
|
||||||
|
case DEACTIVATE:
|
||||||
|
case HARD_DELETE: {
|
||||||
|
final List<BulkActionSupportDAO<?>> dependantSupporterInHierarchicalOrder =
|
||||||
|
getDependantSupporterInHierarchicalOrder(action);
|
||||||
|
Collections.reverse(dependantSupporterInHierarchicalOrder);
|
||||||
|
return dependantSupporterInHierarchicalOrder
|
||||||
|
.stream()
|
||||||
|
.filter(v -> v != null)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return getDependantSupporterInHierarchicalOrder(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<BulkActionSupportDAO<?>> getDependantSupporterInHierarchicalOrder(final BulkAction action) {
|
||||||
|
switch (action.sourceType) {
|
||||||
|
case INSTITUTION:
|
||||||
|
return Arrays.asList(
|
||||||
|
this.supporter.get(EntityType.LMS_SETUP),
|
||||||
|
this.supporter.get(EntityType.USER),
|
||||||
|
this.supporter.get(EntityType.EXAM),
|
||||||
|
this.supporter.get(EntityType.INDICATOR),
|
||||||
|
this.supporter.get(EntityType.CLIENT_CONNECTION),
|
||||||
|
this.supporter.get(EntityType.CONFIGURATION_NODE));
|
||||||
|
case USER:
|
||||||
|
return Arrays.asList(
|
||||||
|
this.supporter.get(EntityType.EXAM),
|
||||||
|
this.supporter.get(EntityType.INDICATOR),
|
||||||
|
this.supporter.get(EntityType.CLIENT_CONNECTION),
|
||||||
|
this.supporter.get(EntityType.CONFIGURATION_NODE));
|
||||||
|
case LMS_SETUP:
|
||||||
|
case EXAM:
|
||||||
|
case CONFIGURATION:
|
||||||
|
return Arrays.asList(
|
||||||
|
this.supporter.get(EntityType.EXAM),
|
||||||
|
this.supporter.get(EntityType.INDICATOR),
|
||||||
|
this.supporter.get(EntityType.CLIENT_CONNECTION));
|
||||||
|
default:
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkProcessing(final BulkAction action) {
|
||||||
|
if (action.alreadyProcessed) {
|
||||||
|
throw new IllegalStateException("Given BulkAction has already been processed. Use a new one");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -25,6 +25,10 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ActivatableEntityDAO;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.DAOLoggingSupport;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.DAOLoggingSupport;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.EntityDAO;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.EntityDAO;
|
||||||
|
|
||||||
|
/** Defines overall DAO support for bulk-actions like activate, deactivate, delete...
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @param <T> The type of the Entity of a concrete BulkActionSupportDAO */
|
||||||
public interface BulkActionSupportDAO<T extends Entity> {
|
public interface BulkActionSupportDAO<T extends Entity> {
|
||||||
|
|
||||||
/** Get the entity type for a concrete EntityDAO implementation.
|
/** Get the entity type for a concrete EntityDAO implementation.
|
||||||
|
@ -32,8 +36,21 @@ public interface BulkActionSupportDAO<T extends Entity> {
|
||||||
* @return The EntityType for a concrete EntityDAO implementation */
|
* @return The EntityType for a concrete EntityDAO implementation */
|
||||||
EntityType entityType();
|
EntityType entityType();
|
||||||
|
|
||||||
|
/** Gets a Set of EntityKey for all dependent entities for a given BulkAction
|
||||||
|
* and the type of this BulkActionSupportDAO.
|
||||||
|
*
|
||||||
|
* @param bulkAction the BulkAction to get keys of dependencies for the concrete type of this BulkActionSupportDAO
|
||||||
|
* @return */
|
||||||
Set<EntityKey> getDependencies(BulkAction bulkAction);
|
Set<EntityKey> getDependencies(BulkAction bulkAction);
|
||||||
|
|
||||||
|
/** This processed a given BulkAction for all entities of the concrete type of this BulkActionSupportDAO
|
||||||
|
* that are defined by this given BulkAction.
|
||||||
|
*
|
||||||
|
* This returns a Collection of EntityKey results of each Entity that has been processed.
|
||||||
|
* If there was an error for a particular Entity, the Result will have an error reference.
|
||||||
|
*
|
||||||
|
* @param bulkAction the BulkAction containing the source entity and all dependencies
|
||||||
|
* @return a Collection of EntityKey results of each Entity that has been processed. */
|
||||||
@Transactional
|
@Transactional
|
||||||
default Collection<Result<EntityKey>> processBulkAction(final BulkAction bulkAction) {
|
default Collection<Result<EntityKey>> processBulkAction(final BulkAction bulkAction) {
|
||||||
final Set<EntityKey> all = bulkAction.extractKeys(entityType());
|
final Set<EntityKey> all = bulkAction.extractKeys(entityType());
|
||||||
|
|
|
@ -8,178 +8,30 @@
|
||||||
|
|
||||||
package ch.ethz.seb.sebserver.webservice.servicelayer.client;
|
package ch.ethz.seb.sebserver.webservice.servicelayer.client;
|
||||||
|
|
||||||
import java.io.UnsupportedEncodingException;
|
|
||||||
import java.security.SecureRandom;
|
|
||||||
|
|
||||||
import org.apache.commons.lang3.RandomStringUtils;
|
|
||||||
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.codec.Hex;
|
|
||||||
import org.springframework.security.crypto.encrypt.Encryptors;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
|
||||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||||
|
|
||||||
@Lazy
|
public interface ClientCredentialService {
|
||||||
@Service
|
|
||||||
@WebServiceProfile
|
|
||||||
public class ClientCredentialService {
|
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(ClientCredentialService.class);
|
Result<ClientCredentials> createGeneratedClientCredentials();
|
||||||
|
|
||||||
static final String SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY = "sebserver.webservice.internalSecret";
|
Result<ClientCredentials> createGeneratedClientCredentials(CharSequence salt);
|
||||||
static final CharSequence DEFAULT_SALT = "b7dbe99bbfa3e21e";
|
|
||||||
|
|
||||||
private final Environment environment;
|
ClientCredentials encryptedClientCredentials(ClientCredentials clientCredentials);
|
||||||
|
|
||||||
protected ClientCredentialService(final Environment environment) {
|
ClientCredentials encryptedClientCredentials(
|
||||||
this.environment = environment;
|
ClientCredentials clientCredentials,
|
||||||
}
|
CharSequence salt);
|
||||||
|
|
||||||
public Result<ClientCredentials> createGeneratedClientCredentials() {
|
CharSequence getPlainClientId(ClientCredentials credentials);
|
||||||
return createGeneratedClientCredentials(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Result<ClientCredentials> createGeneratedClientCredentials(final CharSequence salt) {
|
CharSequence getPlainClientId(ClientCredentials credentials, CharSequence salt);
|
||||||
return Result.tryCatch(() -> {
|
|
||||||
|
|
||||||
try {
|
CharSequence getPlainClientSecret(ClientCredentials credentials);
|
||||||
return encryptedClientCredentials(
|
|
||||||
new ClientCredentials(
|
|
||||||
generateClientId().toString(),
|
|
||||||
generateClientSecret().toString(),
|
|
||||||
null),
|
|
||||||
salt);
|
|
||||||
} catch (final UnsupportedEncodingException e) {
|
|
||||||
log.error("Error while trying to generate client credentials: ", e);
|
|
||||||
throw new RuntimeException("cause: ", e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public ClientCredentials encryptedClientCredentials(final ClientCredentials clientCredentials) {
|
CharSequence getPlainClientSecret(ClientCredentials credentials, CharSequence salt);
|
||||||
return encryptedClientCredentials(clientCredentials, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ClientCredentials encryptedClientCredentials(
|
CharSequence getPlainAccessToken(ClientCredentials credentials);
|
||||||
final ClientCredentials clientCredentials,
|
|
||||||
final CharSequence salt) {
|
|
||||||
|
|
||||||
final CharSequence secret = this.environment
|
CharSequence getPlainAccessToken(ClientCredentials credentials, CharSequence salt);
|
||||||
.getRequiredProperty(SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY);
|
|
||||||
|
|
||||||
return new ClientCredentials(
|
}
|
||||||
(clientCredentials.clientId != null)
|
|
||||||
? encrypt(clientCredentials.clientId, secret, salt).toString()
|
|
||||||
: null,
|
|
||||||
(clientCredentials.secret != null)
|
|
||||||
? encrypt(clientCredentials.secret, secret, salt).toString()
|
|
||||||
: null,
|
|
||||||
(clientCredentials.accessToken != null)
|
|
||||||
? encrypt(clientCredentials.accessToken, secret, salt).toString()
|
|
||||||
: null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public CharSequence getPlainClientId(final ClientCredentials credentials) {
|
|
||||||
return getPlainClientId(credentials, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public CharSequence getPlainClientId(final ClientCredentials credentials, final CharSequence salt) {
|
|
||||||
if (credentials == null || credentials.clientId == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final CharSequence secret = this.environment
|
|
||||||
.getRequiredProperty(SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY);
|
|
||||||
|
|
||||||
return this.decrypt(credentials.clientId, secret, salt);
|
|
||||||
}
|
|
||||||
|
|
||||||
public CharSequence getPlainClientSecret(final ClientCredentials credentials) {
|
|
||||||
return getPlainClientSecret(credentials, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public CharSequence getPlainClientSecret(final ClientCredentials credentials, final CharSequence salt) {
|
|
||||||
if (credentials == null || credentials.secret == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final CharSequence secret = this.environment
|
|
||||||
.getRequiredProperty(SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY);
|
|
||||||
return this.decrypt(credentials.secret, secret, salt);
|
|
||||||
}
|
|
||||||
|
|
||||||
public CharSequence getPlainAccessToken(final ClientCredentials credentials) {
|
|
||||||
return getPlainAccessToken(credentials, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public CharSequence getPlainAccessToken(final ClientCredentials credentials, final CharSequence salt) {
|
|
||||||
if (credentials == null || credentials.accessToken == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final CharSequence secret = this.environment
|
|
||||||
.getRequiredProperty(SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY);
|
|
||||||
return this.decrypt(credentials.accessToken, secret, salt);
|
|
||||||
}
|
|
||||||
|
|
||||||
CharSequence encrypt(final CharSequence text, final CharSequence secret, final CharSequence salt) {
|
|
||||||
if (text == null) {
|
|
||||||
throw new IllegalArgumentException("Text has null reference");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return Encryptors
|
|
||||||
.delux(secret, getSalt(salt))
|
|
||||||
.encrypt(text.toString());
|
|
||||||
|
|
||||||
} catch (final Exception e) {
|
|
||||||
log.error("Failed to encrypt text: ", e);
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CharSequence decrypt(final CharSequence text, final CharSequence secret, final CharSequence salt) {
|
|
||||||
if (text == null) {
|
|
||||||
throw new IllegalArgumentException("Text has null reference");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
|
|
||||||
return Encryptors
|
|
||||||
.delux(secret, getSalt(salt))
|
|
||||||
.decrypt(text.toString());
|
|
||||||
|
|
||||||
} catch (final Exception e) {
|
|
||||||
log.error("Failed to decrypt text: ", e);
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private final static char[] possibleCharacters = (new String(
|
|
||||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789~`!@#$%^&*()-_=+[{]}?"))
|
|
||||||
.toCharArray();
|
|
||||||
|
|
||||||
private CharSequence getSalt(final CharSequence saltPlain) throws UnsupportedEncodingException {
|
|
||||||
final CharSequence _salt = (saltPlain == null || saltPlain.length() <= 0)
|
|
||||||
? this.environment.getProperty(SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY, DEFAULT_SALT.toString())
|
|
||||||
: saltPlain;
|
|
||||||
return new String(Hex.encode(_salt.toString().getBytes("UTF-8")));
|
|
||||||
}
|
|
||||||
|
|
||||||
private CharSequence generateClientId() {
|
|
||||||
return RandomStringUtils.random(
|
|
||||||
16, 0, possibleCharacters.length - 1, false, false,
|
|
||||||
possibleCharacters, new SecureRandom());
|
|
||||||
}
|
|
||||||
|
|
||||||
private CharSequence generateClientSecret() throws UnsupportedEncodingException {
|
|
||||||
return RandomStringUtils.random(
|
|
||||||
64, 0, possibleCharacters.length - 1, false, false,
|
|
||||||
possibleCharacters, new SecureRandom());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -0,0 +1,195 @@
|
||||||
|
/*
|
||||||
|
* 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.security.SecureRandom;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.RandomStringUtils;
|
||||||
|
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.codec.Hex;
|
||||||
|
import org.springframework.security.crypto.encrypt.Encryptors;
|
||||||
|
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";
|
||||||
|
static final CharSequence DEFAULT_SALT = "b7dbe99bbfa3e21e";
|
||||||
|
|
||||||
|
private final Environment environment;
|
||||||
|
|
||||||
|
protected ClientCredentialServiceImpl(final Environment environment) {
|
||||||
|
this.environment = environment;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result<ClientCredentials> createGeneratedClientCredentials() {
|
||||||
|
return createGeneratedClientCredentials(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result<ClientCredentials> createGeneratedClientCredentials(final CharSequence salt) {
|
||||||
|
return Result.tryCatch(() -> {
|
||||||
|
|
||||||
|
try {
|
||||||
|
return encryptedClientCredentials(
|
||||||
|
new ClientCredentials(
|
||||||
|
generateClientId().toString(),
|
||||||
|
generateClientSecret().toString(),
|
||||||
|
null),
|
||||||
|
salt);
|
||||||
|
} catch (final UnsupportedEncodingException e) {
|
||||||
|
log.error("Error while trying to generate client credentials: ", e);
|
||||||
|
throw new RuntimeException("cause: ", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClientCredentials encryptedClientCredentials(final ClientCredentials clientCredentials) {
|
||||||
|
return encryptedClientCredentials(clientCredentials, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClientCredentials encryptedClientCredentials(
|
||||||
|
final ClientCredentials clientCredentials,
|
||||||
|
final CharSequence salt) {
|
||||||
|
|
||||||
|
final CharSequence secret = this.environment
|
||||||
|
.getRequiredProperty(SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY);
|
||||||
|
|
||||||
|
return new ClientCredentials(
|
||||||
|
(clientCredentials.clientId != null)
|
||||||
|
? encrypt(clientCredentials.clientId, secret, salt).toString()
|
||||||
|
: null,
|
||||||
|
(clientCredentials.secret != null)
|
||||||
|
? encrypt(clientCredentials.secret, secret, salt).toString()
|
||||||
|
: null,
|
||||||
|
(clientCredentials.accessToken != null)
|
||||||
|
? encrypt(clientCredentials.accessToken, secret, salt).toString()
|
||||||
|
: null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CharSequence getPlainClientId(final ClientCredentials credentials) {
|
||||||
|
return getPlainClientId(credentials, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CharSequence getPlainClientId(final ClientCredentials credentials, final CharSequence salt) {
|
||||||
|
if (credentials == null || credentials.clientId == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final CharSequence secret = this.environment
|
||||||
|
.getRequiredProperty(SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY);
|
||||||
|
|
||||||
|
return this.decrypt(credentials.clientId, secret, salt);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CharSequence getPlainClientSecret(final ClientCredentials credentials) {
|
||||||
|
return getPlainClientSecret(credentials, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CharSequence getPlainClientSecret(final ClientCredentials credentials, final CharSequence salt) {
|
||||||
|
if (credentials == null || credentials.secret == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final CharSequence secret = this.environment
|
||||||
|
.getRequiredProperty(SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY);
|
||||||
|
return this.decrypt(credentials.secret, secret, salt);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CharSequence getPlainAccessToken(final ClientCredentials credentials) {
|
||||||
|
return getPlainAccessToken(credentials, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CharSequence getPlainAccessToken(final ClientCredentials credentials, final CharSequence salt) {
|
||||||
|
if (credentials == null || credentials.accessToken == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final CharSequence secret = this.environment
|
||||||
|
.getRequiredProperty(SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY);
|
||||||
|
return this.decrypt(credentials.accessToken, secret, salt);
|
||||||
|
}
|
||||||
|
|
||||||
|
CharSequence encrypt(final CharSequence text, final CharSequence secret, final CharSequence salt) {
|
||||||
|
if (text == null) {
|
||||||
|
throw new IllegalArgumentException("Text has null reference");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return Encryptors
|
||||||
|
.delux(secret, getSalt(salt))
|
||||||
|
.encrypt(text.toString());
|
||||||
|
|
||||||
|
} catch (final Exception e) {
|
||||||
|
log.error("Failed to encrypt text: ", e);
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CharSequence decrypt(final CharSequence text, final CharSequence secret, final CharSequence salt) {
|
||||||
|
if (text == null) {
|
||||||
|
throw new IllegalArgumentException("Text has null reference");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
return Encryptors
|
||||||
|
.delux(secret, getSalt(salt))
|
||||||
|
.decrypt(text.toString());
|
||||||
|
|
||||||
|
} catch (final Exception e) {
|
||||||
|
log.error("Failed to decrypt text: ", e);
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final static char[] possibleCharacters = (new String(
|
||||||
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789~`!@#$%^&*()-_=+[{]}?"))
|
||||||
|
.toCharArray();
|
||||||
|
|
||||||
|
private CharSequence getSalt(final CharSequence saltPlain) throws UnsupportedEncodingException {
|
||||||
|
final CharSequence _salt = (saltPlain == null || saltPlain.length() <= 0)
|
||||||
|
? this.environment.getProperty(SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY, DEFAULT_SALT.toString())
|
||||||
|
: saltPlain;
|
||||||
|
return new String(Hex.encode(_salt.toString().getBytes("UTF-8")));
|
||||||
|
}
|
||||||
|
|
||||||
|
private CharSequence generateClientId() {
|
||||||
|
return RandomStringUtils.random(
|
||||||
|
16, 0, possibleCharacters.length - 1, false, false,
|
||||||
|
possibleCharacters, new SecureRandom());
|
||||||
|
}
|
||||||
|
|
||||||
|
private CharSequence generateClientSecret() throws UnsupportedEncodingException {
|
||||||
|
return RandomStringUtils.random(
|
||||||
|
64, 0, possibleCharacters.length - 1, false, false,
|
||||||
|
possibleCharacters, new SecureRandom());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -15,6 +15,7 @@ import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||||
|
|
||||||
|
/** Adds some static logging support for DAO's */
|
||||||
public final class DAOLoggingSupport {
|
public final class DAOLoggingSupport {
|
||||||
|
|
||||||
public static final Logger log = LoggerFactory.getLogger(DAOLoggingSupport.class);
|
public static final Logger log = LoggerFactory.getLogger(DAOLoggingSupport.class);
|
||||||
|
|
|
@ -13,8 +13,10 @@ import org.springframework.util.LinkedMultiValueMap;
|
||||||
import org.springframework.util.MultiValueMap;
|
import org.springframework.util.MultiValueMap;
|
||||||
|
|
||||||
import ch.ethz.seb.sebserver.gbl.api.POSTMapper;
|
import ch.ethz.seb.sebserver.gbl.api.POSTMapper;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.model.Entity;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
|
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.exam.Indicator;
|
import ch.ethz.seb.sebserver.gbl.model.exam.Indicator;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
|
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.institution.SebClientConfig;
|
import ch.ethz.seb.sebserver.gbl.model.institution.SebClientConfig;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.user.UserInfo;
|
import ch.ethz.seb.sebserver.gbl.model.user.UserInfo;
|
||||||
|
@ -39,7 +41,7 @@ public class FilterMap extends POSTMapper {
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getName() {
|
public String getName() {
|
||||||
return getSQLWildcard(UserInfo.FILTER_ATTR_NAME);
|
return getSQLWildcard(Entity.FILTER_ATTR_NAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getUserUsername() {
|
public String getUserUsername() {
|
||||||
|
@ -54,14 +56,14 @@ public class FilterMap extends POSTMapper {
|
||||||
return getString(UserInfo.FILTER_ATTR_LANGUAGE);
|
return getString(UserInfo.FILTER_ATTR_LANGUAGE);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getLmsSetupName() {
|
|
||||||
return getSQLWildcard(LmsSetup.FILTER_ATTR_NAME);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getLmsSetupType() {
|
public String getLmsSetupType() {
|
||||||
return getString(LmsSetup.FILTER_ATTR_LMS_TYPE);
|
return getString(LmsSetup.FILTER_ATTR_LMS_TYPE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public DateTime getQuizFromTime() {
|
||||||
|
return JodaTimeTypeResolver.getDateTime(getString(QuizData.FILTER_ATTR_START_TIME));
|
||||||
|
}
|
||||||
|
|
||||||
public DateTime getExamFromTime() {
|
public DateTime getExamFromTime() {
|
||||||
return JodaTimeTypeResolver.getDateTime(getString(Exam.FILTER_ATTR_FROM));
|
return JodaTimeTypeResolver.getDateTime(getString(Exam.FILTER_ATTR_FROM));
|
||||||
}
|
}
|
||||||
|
@ -74,8 +76,8 @@ public class FilterMap extends POSTMapper {
|
||||||
return getString(Exam.FILTER_ATTR_QUIZ_ID);
|
return getString(Exam.FILTER_ATTR_QUIZ_ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Long getExamLmsSetupId() {
|
public Long getLmsSetupId() {
|
||||||
return getLong(Exam.FILTER_ATTR_LMS_SETUP);
|
return getLong(LmsSetup.FILTER_ATTR_LMS_SETUP);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getExamStatus() {
|
public String getExamStatus() {
|
||||||
|
|
|
@ -134,7 +134,7 @@ public class ExamDAOImpl implements ExamDAO {
|
||||||
isEqualToWhenPresent(filterMap.getExamQuizId()))
|
isEqualToWhenPresent(filterMap.getExamQuizId()))
|
||||||
.and(
|
.and(
|
||||||
ExamRecordDynamicSqlSupport.lmsSetupId,
|
ExamRecordDynamicSqlSupport.lmsSetupId,
|
||||||
isEqualToWhenPresent(filterMap.getExamLmsSetupId()))
|
isEqualToWhenPresent(filterMap.getLmsSetupId()))
|
||||||
.and(
|
.and(
|
||||||
ExamRecordDynamicSqlSupport.status,
|
ExamRecordDynamicSqlSupport.status,
|
||||||
isEqualToWhenPresent(filterMap.getExamStatus()))
|
isEqualToWhenPresent(filterMap.getExamStatus()))
|
||||||
|
@ -366,7 +366,7 @@ public class ExamDAOImpl implements ExamDAO {
|
||||||
(map1, map2) -> Utils.mapPutAll(map1, map2));
|
(map1, map2) -> Utils.mapPutAll(map1, map2));
|
||||||
|
|
||||||
return this.lmsAPIService
|
return this.lmsAPIService
|
||||||
.createLmsAPITemplate(lmsSetupId)
|
.getLmsAPITemplate(lmsSetupId)
|
||||||
.map(template -> template.getQuizzes(recordMapping.keySet()))
|
.map(template -> template.getQuizzes(recordMapping.keySet()))
|
||||||
.getOrThrow()
|
.getOrThrow()
|
||||||
.stream()
|
.stream()
|
||||||
|
|
|
@ -110,7 +110,7 @@ public class LmsSetupDAOImpl implements LmsSetupDAO {
|
||||||
isEqualToWhenPresent(filterMap.getInstitutionId()))
|
isEqualToWhenPresent(filterMap.getInstitutionId()))
|
||||||
.and(
|
.and(
|
||||||
LmsSetupRecordDynamicSqlSupport.name,
|
LmsSetupRecordDynamicSqlSupport.name,
|
||||||
isLikeWhenPresent(filterMap.getLmsSetupName()))
|
isLikeWhenPresent(filterMap.getName()))
|
||||||
.and(
|
.and(
|
||||||
LmsSetupRecordDynamicSqlSupport.lmsType,
|
LmsSetupRecordDynamicSqlSupport.lmsType,
|
||||||
isEqualToWhenPresent(filterMap.getLmsSetupType()))
|
isEqualToWhenPresent(filterMap.getLmsSetupType()))
|
||||||
|
|
|
@ -8,13 +8,84 @@
|
||||||
|
|
||||||
package ch.ethz.seb.sebserver.webservice.servicelayer.lms;
|
package ch.ethz.seb.sebserver.webservice.servicelayer.lms;
|
||||||
|
|
||||||
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
|
import java.util.List;
|
||||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
import java.util.function.Function;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.joda.time.DateTime;
|
||||||
|
|
||||||
|
import ch.ethz.seb.sebserver.gbl.model.Page;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||||
|
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
|
||||||
|
|
||||||
|
/** Defines the LMS API access service interface with all functionality needed to access
|
||||||
|
* a LMS API within a given LmsSetup configuration.
|
||||||
|
*
|
||||||
|
* There are LmsAPITemplate implementations for each type of supported LMS that are managed
|
||||||
|
* in reference to a LmsSetup configuration within this service. This means actually that
|
||||||
|
* this service caches requested LmsAPITemplate (that holds the LMS API connection) as long
|
||||||
|
* as there is no change in the underling LmsSetup configuration. If the LmsSetup configuration
|
||||||
|
* changes this service will be notifies about the change and release the related LmsAPITemplate from cache. */
|
||||||
public interface LmsAPIService {
|
public interface LmsAPIService {
|
||||||
|
|
||||||
Result<LmsAPITemplate> createLmsAPITemplate(Long lmsSetupId);
|
Result<Page<QuizData>> requestQuizDataPage(
|
||||||
|
final int pageNumber,
|
||||||
|
final int pageSize,
|
||||||
|
final String sort,
|
||||||
|
final FilterMap filterMap);
|
||||||
|
|
||||||
Result<LmsAPITemplate> createLmsAPITemplate(LmsSetup lmsSetup);
|
/** Get a LmsAPITemplate for specified LmsSetup configuration.
|
||||||
|
*
|
||||||
|
* @param lmsSetupId the identifier of LmsSetup
|
||||||
|
* @return LmsAPITemplate for specified LmsSetup configuration */
|
||||||
|
Result<LmsAPITemplate> getLmsAPITemplate(String lmsSetupId);
|
||||||
|
|
||||||
|
default Result<LmsAPITemplate> getLmsAPITemplate(final Long lmsSetupId) {
|
||||||
|
if (lmsSetupId == null) {
|
||||||
|
return Result.ofError(new IllegalArgumentException("lmsSetupId has null-reference"));
|
||||||
|
}
|
||||||
|
return getLmsAPITemplate(String.valueOf(lmsSetupId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Predicate<QuizData> quizzeFilterFunction(final FilterMap filterMap) {
|
||||||
|
final String name = filterMap.getName();
|
||||||
|
final DateTime from = filterMap.getQuizFromTime();
|
||||||
|
return q -> (StringUtils.isBlank(name) || (q.name != null && q.name.contains(name)))
|
||||||
|
&& (from == null) || (q.startTime != null && q.startTime.isBefore(from));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Function<List<QuizData>, List<QuizData>> quizzesFilterFunction(final FilterMap filterMap) {
|
||||||
|
filterMap.getName();
|
||||||
|
return quizzes -> quizzes
|
||||||
|
.stream()
|
||||||
|
.filter(quizzeFilterFunction(filterMap))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Function<List<QuizData>, Page<QuizData>> quizzesToPageFunction(
|
||||||
|
final String sort,
|
||||||
|
final int pageNumber,
|
||||||
|
final int pageSize) {
|
||||||
|
|
||||||
|
return quizzes -> {
|
||||||
|
final int start = pageNumber * pageSize;
|
||||||
|
int end = start + pageSize;
|
||||||
|
if (end > quizzes.size() - 1) {
|
||||||
|
end = quizzes.size() - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Page<>(quizzes.size() / pageSize, pageNumber, sort, quizzes.subList(start, end));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Function<List<QuizData>, List<QuizData>> quizzesSortFunction(final String sort) {
|
||||||
|
return quizzes -> {
|
||||||
|
quizzes.sort(QuizData.getComparator(sort));
|
||||||
|
return quizzes;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,33 +8,77 @@
|
||||||
|
|
||||||
package ch.ethz.seb.sebserver.webservice.servicelayer.lms;
|
package ch.ethz.seb.sebserver.webservice.servicelayer.lms;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import ch.ethz.seb.sebserver.gbl.model.Page;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
|
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.model.Domain.LMS_SETUP;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
|
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
|
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
|
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails;
|
import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails;
|
||||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||||
|
import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentials;
|
||||||
|
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
|
||||||
|
|
||||||
|
/** Defines the interface to an LMS within a specified LMSSetup configuration.
|
||||||
|
* There is one concrete implementations for every supported type of LMS like
|
||||||
|
* Open edX or Moodle
|
||||||
|
*
|
||||||
|
* A LmsAPITemplate defines at least the core API access to query courses and quizzes from the LMS
|
||||||
|
* Later a concrete LmsAPITemplate may also implement some special features regarding to the type
|
||||||
|
* of LMS */
|
||||||
public interface LmsAPITemplate {
|
public interface LmsAPITemplate {
|
||||||
|
|
||||||
Result<LmsSetup> lmsSetup();
|
/** Get the underling LMSSetup configuration for this LmsAPITemplate
|
||||||
|
*
|
||||||
|
* @return the underling LMSSetup configuration for this LmsAPITemplate */
|
||||||
|
LmsSetup lmsSetup();
|
||||||
|
|
||||||
|
/** Performs a test for the underling LmsSetup configuration and checks if the
|
||||||
|
* LMS and the core API of the LMS can be accessed or if there are some difficulties,
|
||||||
|
* missing configuration data or connection/authentication errors.
|
||||||
|
*
|
||||||
|
* @return LmsSetupTestResult instance with the test result report */
|
||||||
LmsSetupTestResult testLmsSetup();
|
LmsSetupTestResult testLmsSetup();
|
||||||
|
|
||||||
Result<Page<QuizData>> getQuizzes(
|
/** Get a Result of an unsorted List of filtered QuizData from the LMS course/quiz API
|
||||||
String name,
|
*
|
||||||
Long from,
|
* @param filterMap the FilterMap to get a filtered result. For possible filter attributes
|
||||||
String sort,
|
* see documentation on QuizData
|
||||||
int pageNumber,
|
* @return Result of an unsorted List of filtered QuizData from the LMS course/quiz API
|
||||||
int pageSize);
|
* or refer to an error when happened */
|
||||||
|
Result<List<QuizData>> getQuizzes(FilterMap filterMap);
|
||||||
|
|
||||||
Collection<Result<QuizData>> getQuizzes(Set<String> ids);
|
Collection<Result<QuizData>> getQuizzes(Set<String> ids);
|
||||||
|
|
||||||
Result<ExamineeAccountDetails> getExamineeAccountDetails(String examineeUserId);
|
Result<ExamineeAccountDetails> getExamineeAccountDetails(String examineeUserId);
|
||||||
|
|
||||||
void reset();
|
default List<APIMessage> attributeValidation(final ClientCredentials credentials) {
|
||||||
|
|
||||||
|
final LmsSetup lmsSetup = lmsSetup();
|
||||||
|
// validation of LmsSetup
|
||||||
|
final List<APIMessage> missingAttrs = new ArrayList<>();
|
||||||
|
if (StringUtils.isBlank(lmsSetup.lmsApiUrl)) {
|
||||||
|
missingAttrs.add(APIMessage.fieldValidationError(
|
||||||
|
LMS_SETUP.ATTR_LMS_URL,
|
||||||
|
"lmsSetup:lmsUrl:notNull"));
|
||||||
|
}
|
||||||
|
if (StringUtils.isBlank(credentials.clientId)) {
|
||||||
|
missingAttrs.add(APIMessage.fieldValidationError(
|
||||||
|
LMS_SETUP.ATTR_LMS_CLIENTNAME,
|
||||||
|
"lmsSetup:lmsClientname:notNull"));
|
||||||
|
}
|
||||||
|
if (StringUtils.isBlank(credentials.secret)) {
|
||||||
|
missingAttrs.add(APIMessage.fieldValidationError(
|
||||||
|
LMS_SETUP.ATTR_LMS_CLIENTSECRET,
|
||||||
|
"lmsSetup:lmsClientsecret:notNull"));
|
||||||
|
}
|
||||||
|
return missingAttrs;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,39 +8,57 @@
|
||||||
|
|
||||||
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl;
|
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
|
import org.springframework.context.event.EventListener;
|
||||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import ch.ethz.seb.sebserver.gbl.Constants;
|
import ch.ethz.seb.sebserver.gbl.Constants;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.model.Page;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
|
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
|
||||||
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
||||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentialService;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentialService;
|
||||||
|
import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentials;
|
||||||
|
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.LmsSetupDAO;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.LmsSetupDAO;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
|
||||||
|
import ch.ethz.seb.sebserver.webservice.weblayer.api.IllegalAPIArgumentException;
|
||||||
|
|
||||||
|
@Lazy
|
||||||
@Service
|
@Service
|
||||||
@WebServiceProfile
|
@WebServiceProfile
|
||||||
public class LmsAPIServiceImpl implements LmsAPIService {
|
public class LmsAPIServiceImpl implements LmsAPIService {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(LmsAPIServiceImpl.class);
|
||||||
|
|
||||||
private final LmsSetupDAO lmsSetupDAO;
|
private final LmsSetupDAO lmsSetupDAO;
|
||||||
private final ClientCredentialService internalEncryptionService;
|
private final ClientCredentialService clientCredentialService;
|
||||||
private final ClientHttpRequestFactory clientHttpRequestFactory;
|
private final ClientHttpRequestFactory clientHttpRequestFactory;
|
||||||
private final String[] openEdxAlternativeTokenRequestPaths;
|
private final String[] openEdxAlternativeTokenRequestPaths;
|
||||||
|
|
||||||
// TODO internal caching of LmsAPITemplate per LmsSetup (Id)
|
private final Map<CacheKey, LmsAPITemplate> cache = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
public LmsAPIServiceImpl(
|
public LmsAPIServiceImpl(
|
||||||
final LmsSetupDAO lmsSetupDAO,
|
final LmsSetupDAO lmsSetupDAO,
|
||||||
final ClientCredentialService internalEncryptionService,
|
final ClientCredentialService clientCredentialService,
|
||||||
final ClientHttpRequestFactory clientHttpRequestFactory,
|
final ClientHttpRequestFactory clientHttpRequestFactory,
|
||||||
@Value("${sebserver.lms.openedix.api.token.request.paths}") final String alternativeTokenRequestPaths) {
|
@Value("${sebserver.lms.openedix.api.token.request.paths}") final String alternativeTokenRequestPaths) {
|
||||||
|
|
||||||
this.lmsSetupDAO = lmsSetupDAO;
|
this.lmsSetupDAO = lmsSetupDAO;
|
||||||
this.internalEncryptionService = internalEncryptionService;
|
this.clientCredentialService = clientCredentialService;
|
||||||
this.clientHttpRequestFactory = clientHttpRequestFactory;
|
this.clientHttpRequestFactory = clientHttpRequestFactory;
|
||||||
|
|
||||||
this.openEdxAlternativeTokenRequestPaths = (alternativeTokenRequestPaths != null)
|
this.openEdxAlternativeTokenRequestPaths = (alternativeTokenRequestPaths != null)
|
||||||
|
@ -48,60 +66,167 @@ public class LmsAPIServiceImpl implements LmsAPIService {
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
/** Listen to LmsSetupChangeEvent to release an affected LmsAPITemplate from cache
|
||||||
public Result<LmsAPITemplate> createLmsAPITemplate(final Long lmsSetupId) {
|
*
|
||||||
return this.lmsSetupDAO
|
* @param event the event holding the changed LmsSetup */
|
||||||
.byPK(lmsSetupId)
|
@EventListener
|
||||||
.flatMap(this::createLmsAPITemplate);
|
public void notifyLmsSetupChange(final LmsSetupChangeEvent event) {
|
||||||
}
|
final LmsSetup lmsSetup = event.getLmsSetup();
|
||||||
|
if (lmsSetup == null) {
|
||||||
@Override
|
return;
|
||||||
public Result<LmsAPITemplate> createLmsAPITemplate(final LmsSetup lmsSetup) {
|
|
||||||
switch (lmsSetup.lmsType) {
|
|
||||||
case MOCKUP:
|
|
||||||
return Result.of(new MockupLmsAPITemplate(
|
|
||||||
this.lmsSetupDAO,
|
|
||||||
lmsSetup,
|
|
||||||
this.internalEncryptionService));
|
|
||||||
case OPEN_EDX:
|
|
||||||
return Result.of(new OpenEdxLmsAPITemplate(
|
|
||||||
lmsSetup.getModelId(),
|
|
||||||
this.lmsSetupDAO,
|
|
||||||
this.internalEncryptionService,
|
|
||||||
this.clientHttpRequestFactory,
|
|
||||||
this.openEdxAlternativeTokenRequestPaths));
|
|
||||||
default:
|
|
||||||
return Result.ofError(
|
|
||||||
new UnsupportedOperationException("No support for LMS Type: " + lmsSetup.lmsType));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.debug("LmsSetup changed. Update cache by removeing eventually used references");
|
||||||
|
|
||||||
|
this.cache.remove(new CacheKey(lmsSetup.getModelId(), 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Override
|
@Override
|
||||||
// public Result<InputStream> createSEBStartConfiguration(final Long lmsSetupId) {
|
public Result<Page<QuizData>> requestQuizDataPage(
|
||||||
// return this.lmsSetupDAO
|
final int pageNumber,
|
||||||
// .byPK(lmsSetupId)
|
final int pageSize,
|
||||||
// .flatMap(this::createSEBStartConfiguration);
|
final String sort,
|
||||||
// }
|
final FilterMap filterMap) {
|
||||||
//
|
|
||||||
// @Override
|
return getAllQuizzesFromLMSSetups(filterMap)
|
||||||
// public Result<InputStream> createSEBStartConfiguration(final LmsSetup lmsSetup) {
|
.map(LmsAPIService.quizzesSortFunction(sort))
|
||||||
//
|
.map(LmsAPIService.quizzesToPageFunction(sort, pageNumber, pageSize));
|
||||||
// // TODO implementation of creation of SEB start configuration for specified LmsSetup
|
}
|
||||||
// // A SEB start configuration should at least contain the SEB-Client-Credentials to access the SEB Server API
|
|
||||||
// // and the SEB Server URL
|
/** Collect all QuizData from all affecting LmsSetup.
|
||||||
// //
|
* If filterMap contains a LmsSetup identifier, only the QuizData from that LmsSetup is collected.
|
||||||
// // To Clarify : The format of a SEB start configuration
|
* Otherwise QuizData from all active LmsSetup of the current institution are collected.
|
||||||
// // To Clarify : How the file should be encrypted (use case) maybe we need another encryption-secret for this that can be given by
|
*
|
||||||
// // an administrator on SEB start configuration creation time
|
* @param filterMap the FilterMap containing either an LmsSetup identifier or an institution identifier
|
||||||
//
|
* @return list of QuizData from all affecting LmsSetup */
|
||||||
// return Result.tryCatch(() -> {
|
private Result<List<QuizData>> getAllQuizzesFromLMSSetups(final FilterMap filterMap) {
|
||||||
// try {
|
|
||||||
// return new ByteArrayInputStream("TODO".getBytes("UTF-8"));
|
return Result.tryCatch(() -> {
|
||||||
// } catch (final UnsupportedEncodingException e) {
|
final Long lmsSetupId = filterMap.getLmsSetupId();
|
||||||
// throw new RuntimeException("cause: ", e);
|
if (lmsSetupId != null) {
|
||||||
// }
|
return getLmsAPITemplate(lmsSetupId)
|
||||||
// });
|
.getOrThrow()
|
||||||
// }
|
.getQuizzes(filterMap)
|
||||||
|
.getOrThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
final Long institutionId = filterMap.getInstitutionId();
|
||||||
|
if (institutionId == null) {
|
||||||
|
throw new IllegalAPIArgumentException("Missing institution identifier");
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.lmsSetupDAO.all(institutionId, true)
|
||||||
|
.getOrThrow()
|
||||||
|
.stream()
|
||||||
|
.map(this::getLmsAPITemplate)
|
||||||
|
.flatMap(Result::onErrorLogAndSkip)
|
||||||
|
.map(template -> template.getQuizzes(filterMap))
|
||||||
|
.flatMap(Result::onErrorLogAndSkip)
|
||||||
|
.flatMap(List::stream)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result<LmsAPITemplate> getLmsAPITemplate(final String lmsSetupId) {
|
||||||
|
|
||||||
|
log.debug("Get LmsAPITemplate for id: {}", lmsSetupId);
|
||||||
|
|
||||||
|
return Result.tryCatch(() -> {
|
||||||
|
return this.lmsSetupDAO
|
||||||
|
.byModelId(lmsSetupId)
|
||||||
|
.getOrThrow();
|
||||||
|
})
|
||||||
|
.flatMap(this::getLmsAPITemplate);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Result<LmsAPITemplate> getLmsAPITemplate(final LmsSetup lmsSetup) {
|
||||||
|
return Result.tryCatch(() -> {
|
||||||
|
LmsAPITemplate lmsAPITemplate = getFromCache(lmsSetup);
|
||||||
|
if (lmsAPITemplate == null) {
|
||||||
|
log.debug("Get cached LmsAPITemplate with id: {}", lmsSetup.getModelId());
|
||||||
|
return lmsAPITemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
lmsAPITemplate = createLmsSetupTemplate(lmsSetup);
|
||||||
|
this.cache.put(new CacheKey(lmsSetup.getModelId(), System.currentTimeMillis()), lmsAPITemplate);
|
||||||
|
return lmsAPITemplate;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private LmsAPITemplate getFromCache(final LmsSetup lmsSetup) {
|
||||||
|
// first cleanup the cache by removing old instances
|
||||||
|
final long currentTimeMillis = System.currentTimeMillis();
|
||||||
|
new ArrayList<>(this.cache.keySet())
|
||||||
|
.stream()
|
||||||
|
.filter(key -> key.creationTimestamp - currentTimeMillis > Constants.DAY_IN_MILLIS)
|
||||||
|
.forEach(key -> this.cache.remove(key));
|
||||||
|
// get from cache
|
||||||
|
return this.cache.get(new CacheKey(lmsSetup.getModelId(), 0));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private LmsAPITemplate createLmsSetupTemplate(final LmsSetup lmsSetup) {
|
||||||
|
|
||||||
|
log.debug("Create new LmsAPITemplate for id: {}", lmsSetup.getModelId());
|
||||||
|
|
||||||
|
final ClientCredentials credentials = this.lmsSetupDAO
|
||||||
|
.getLmsAPIAccessCredentials(lmsSetup.getModelId())
|
||||||
|
.getOrThrow();
|
||||||
|
|
||||||
|
switch (lmsSetup.lmsType) {
|
||||||
|
case MOCKUP:
|
||||||
|
return new MockupLmsAPITemplate(
|
||||||
|
lmsSetup,
|
||||||
|
credentials,
|
||||||
|
this.clientCredentialService);
|
||||||
|
case OPEN_EDX:
|
||||||
|
return new OpenEdxLmsAPITemplate(
|
||||||
|
lmsSetup,
|
||||||
|
credentials,
|
||||||
|
this.clientCredentialService,
|
||||||
|
this.clientHttpRequestFactory,
|
||||||
|
this.openEdxAlternativeTokenRequestPaths);
|
||||||
|
default:
|
||||||
|
throw new UnsupportedOperationException("No support for LMS Type: " + lmsSetup.lmsType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class CacheKey {
|
||||||
|
final String lmsSetupId;
|
||||||
|
final long creationTimestamp;
|
||||||
|
final int hash;
|
||||||
|
|
||||||
|
CacheKey(final String lmsSetupId, final long creationTimestamp) {
|
||||||
|
this.lmsSetupId = lmsSetupId;
|
||||||
|
this.creationTimestamp = creationTimestamp;
|
||||||
|
final int prime = 31;
|
||||||
|
int result = 1;
|
||||||
|
result = prime * result + ((lmsSetupId == null) ? 0 : lmsSetupId.hashCode());
|
||||||
|
this.hash = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return this.hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(final Object obj) {
|
||||||
|
if (this == obj)
|
||||||
|
return true;
|
||||||
|
if (obj == null)
|
||||||
|
return false;
|
||||||
|
if (getClass() != obj.getClass())
|
||||||
|
return false;
|
||||||
|
final CacheKey other = (CacheKey) obj;
|
||||||
|
if (this.lmsSetupId == null) {
|
||||||
|
if (other.lmsSetupId != null)
|
||||||
|
return false;
|
||||||
|
} else if (!this.lmsSetupId.equals(other.lmsSetupId))
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
/*
|
||||||
|
* 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.lms.impl;
|
||||||
|
|
||||||
|
import org.springframework.context.ApplicationEvent;
|
||||||
|
|
||||||
|
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
|
||||||
|
|
||||||
|
public class LmsSetupChangeEvent extends ApplicationEvent {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = -7239994198026689531L;
|
||||||
|
|
||||||
|
public LmsSetupChangeEvent(final LmsSetup source) {
|
||||||
|
super(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LmsSetup getLmsSetup() {
|
||||||
|
return (LmsSetup) this.source;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -10,50 +10,46 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import ch.ethz.seb.sebserver.gbl.model.Domain.LMS_SETUP;
|
import org.slf4j.Logger;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.Page;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
|
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
|
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
|
|
||||||
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
|
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails;
|
import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails;
|
||||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.PaginationService.SortOrder;
|
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentialService;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentialService;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentials;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentials;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.LmsSetupDAO;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
|
||||||
|
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
|
||||||
|
|
||||||
final class MockupLmsAPITemplate implements LmsAPITemplate {
|
final class MockupLmsAPITemplate implements LmsAPITemplate {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(MockupLmsAPITemplate.class);
|
||||||
|
|
||||||
public static final String MOCKUP_LMS_CLIENT_NAME = "mockupLmsClientName";
|
public static final String MOCKUP_LMS_CLIENT_NAME = "mockupLmsClientName";
|
||||||
public static final String MOCKUP_LMS_CLIENT_SECRET = "mockupLmsClientSecret";
|
public static final String MOCKUP_LMS_CLIENT_SECRET = "mockupLmsClientSecret";
|
||||||
|
|
||||||
private final ClientCredentialService clientCredentialService;
|
private final ClientCredentialService clientCredentialService;
|
||||||
private final LmsSetupDAO lmsSetupDao;
|
private final LmsSetup lmsSetup;
|
||||||
private final LmsSetup setup;
|
private final ClientCredentials credentials;
|
||||||
|
|
||||||
private ClientCredentials credentials = null;
|
|
||||||
private final Collection<QuizData> mockups;
|
private final Collection<QuizData> mockups;
|
||||||
|
|
||||||
MockupLmsAPITemplate(
|
MockupLmsAPITemplate(
|
||||||
final LmsSetupDAO lmsSetupDao,
|
final LmsSetup lmsSetup,
|
||||||
final LmsSetup setup,
|
final ClientCredentials credentials,
|
||||||
final ClientCredentialService clientCredentialService) {
|
final ClientCredentialService clientCredentialService) {
|
||||||
|
|
||||||
this.lmsSetupDao = lmsSetupDao;
|
this.lmsSetup = lmsSetup;
|
||||||
this.clientCredentialService = clientCredentialService;
|
this.clientCredentialService = clientCredentialService;
|
||||||
if (!setup.isActive() || setup.lmsType != LmsType.MOCKUP) {
|
this.credentials = credentials;
|
||||||
throw new IllegalArgumentException();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setup = setup;
|
|
||||||
this.mockups = new ArrayList<>();
|
this.mockups = new ArrayList<>();
|
||||||
this.mockups.add(new QuizData(
|
this.mockups.add(new QuizData(
|
||||||
"quiz1", "Demo Quiz 1", "Demo Quit Mockup",
|
"quiz1", "Demo Quiz 1", "Demo Quit Mockup",
|
||||||
|
@ -79,88 +75,45 @@ final class MockupLmsAPITemplate implements LmsAPITemplate {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Result<LmsSetup> lmsSetup() {
|
public LmsSetup lmsSetup() {
|
||||||
return Result.of(this.setup);
|
return this.lmsSetup;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public LmsSetupTestResult testLmsSetup() {
|
public LmsSetupTestResult testLmsSetup() {
|
||||||
if (this.setup.lmsType != LmsType.MOCKUP) {
|
|
||||||
return LmsSetupTestResult.ofMissingAttributes(LMS_SETUP.ATTR_LMS_TYPE);
|
log.info("Test Lms Binding for Mockup and LmsSetup: {}", this.lmsSetup);
|
||||||
|
|
||||||
|
final List<APIMessage> missingAttrs = attributeValidation(this.credentials);
|
||||||
|
if (!missingAttrs.isEmpty()) {
|
||||||
|
return LmsSetupTestResult.ofMissingAttributes(missingAttrs);
|
||||||
}
|
}
|
||||||
initCredentials();
|
|
||||||
if (this.credentials != null) {
|
if (authenticate()) {
|
||||||
return LmsSetupTestResult.ofOkay();
|
return LmsSetupTestResult.ofOkay();
|
||||||
} else {
|
} else {
|
||||||
return LmsSetupTestResult.ofMissingAttributes(
|
return LmsSetupTestResult.ofTokenRequestError("Illegal access");
|
||||||
LMS_SETUP.ATTR_LMS_URL,
|
|
||||||
LMS_SETUP.ATTR_LMS_CLIENTNAME,
|
|
||||||
LMS_SETUP.ATTR_LMS_CLIENTSECRET);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Collection<QuizData> getQuizzes(
|
|
||||||
final String name,
|
|
||||||
final Long from,
|
|
||||||
final String sort) {
|
|
||||||
|
|
||||||
final int orderFactor = (SortOrder.getSortOrder(sort) == SortOrder.DESCENDING)
|
|
||||||
? -1
|
|
||||||
: 1;
|
|
||||||
|
|
||||||
final String _sort = SortOrder.decode(sort);
|
|
||||||
final Comparator<QuizData> comp = (_sort != null)
|
|
||||||
? (_sort.equals(QuizData.FILTER_ATTR_START_TIME))
|
|
||||||
? (q1, q2) -> q1.startTime.compareTo(q2.startTime) * orderFactor
|
|
||||||
: (q1, q2) -> q1.name.compareTo(q2.name) * orderFactor
|
|
||||||
: (q1, q2) -> q1.name.compareTo(q2.name) * orderFactor;
|
|
||||||
|
|
||||||
return this.mockups.stream()
|
|
||||||
.filter(mockup -> (name != null)
|
|
||||||
? mockup.name.contains(name)
|
|
||||||
: true && (from != null)
|
|
||||||
? mockup.startTime.getMillis() >= from
|
|
||||||
: true)
|
|
||||||
.sorted(comp)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Result<Page<QuizData>> getQuizzes(
|
public Result<List<QuizData>> getQuizzes(final FilterMap filterMap) {
|
||||||
final String name,
|
|
||||||
final Long from,
|
|
||||||
final String sort,
|
|
||||||
final int pageNumber,
|
|
||||||
final int pageSize) {
|
|
||||||
|
|
||||||
return Result.tryCatch(() -> {
|
return Result.tryCatch(() -> {
|
||||||
initCredentials();
|
authenticate();
|
||||||
if (this.credentials == null) {
|
if (this.credentials == null) {
|
||||||
throw new IllegalArgumentException("Wrong clientId or secret");
|
throw new IllegalArgumentException("Wrong clientId or secret");
|
||||||
}
|
}
|
||||||
|
|
||||||
final int startIndex = pageNumber * pageSize;
|
return this.mockups.stream()
|
||||||
final int endIndex = startIndex + pageSize;
|
.filter(LmsAPIService.quizzeFilterFunction(filterMap))
|
||||||
int index = 0;
|
.collect(Collectors.toList());
|
||||||
final Collection<QuizData> quizzes = getQuizzes(name, from, sort);
|
|
||||||
final int numberOfPages = quizzes.size() / pageSize;
|
|
||||||
final Iterator<QuizData> iterator = quizzes.iterator();
|
|
||||||
final List<QuizData> pageContent = new ArrayList<>();
|
|
||||||
while (iterator.hasNext() && index < endIndex) {
|
|
||||||
final QuizData next = iterator.next();
|
|
||||||
if (index >= startIndex) {
|
|
||||||
pageContent.add(next);
|
|
||||||
}
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Page<>(numberOfPages, pageNumber, sort, pageContent);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Collection<Result<QuizData>> getQuizzes(final Set<String> ids) {
|
public Collection<Result<QuizData>> getQuizzes(final Set<String> ids) {
|
||||||
initCredentials();
|
authenticate();
|
||||||
if (this.credentials == null) {
|
if (this.credentials == null) {
|
||||||
throw new IllegalArgumentException("Wrong clientId or secret");
|
throw new IllegalArgumentException("Wrong clientId or secret");
|
||||||
}
|
}
|
||||||
|
@ -173,7 +126,7 @@ final class MockupLmsAPITemplate implements LmsAPITemplate {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeUserId) {
|
public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeUserId) {
|
||||||
initCredentials();
|
authenticate();
|
||||||
if (this.credentials == null) {
|
if (this.credentials == null) {
|
||||||
throw new IllegalArgumentException("Wrong clientId or secret");
|
throw new IllegalArgumentException("Wrong clientId or secret");
|
||||||
}
|
}
|
||||||
|
@ -181,28 +134,23 @@ final class MockupLmsAPITemplate implements LmsAPITemplate {
|
||||||
return Result.of(new ExamineeAccountDetails(examineeUserId, "mockup", "mockup", "mockup"));
|
return Result.of(new ExamineeAccountDetails(examineeUserId, "mockup", "mockup", "mockup"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private boolean authenticate() {
|
||||||
public void reset() {
|
|
||||||
this.credentials = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initCredentials() {
|
|
||||||
try {
|
try {
|
||||||
this.credentials = this.lmsSetupDao
|
|
||||||
.getLmsAPIAccessCredentials(this.setup.getModelId())
|
|
||||||
.getOrThrow();
|
|
||||||
|
|
||||||
final CharSequence plainClientId = this.clientCredentialService.getPlainClientId(this.credentials);
|
final CharSequence plainClientId = this.clientCredentialService.getPlainClientId(this.credentials);
|
||||||
if (!"lmsMockupClientId".equals(plainClientId)) {
|
if (!"lmsMockupClientId".equals(plainClientId)) {
|
||||||
throw new IllegalAccessError();
|
throw new IllegalAccessException("Wrong client credential");
|
||||||
}
|
}
|
||||||
|
|
||||||
final CharSequence plainClientSecret = this.clientCredentialService.getPlainClientSecret(this.credentials);
|
final CharSequence plainClientSecret = this.clientCredentialService.getPlainClientSecret(this.credentials);
|
||||||
if (!"lmsMockupSecret".equals(plainClientSecret)) {
|
if (!"lmsMockupSecret".equals(plainClientSecret)) {
|
||||||
throw new IllegalAccessError();
|
throw new IllegalAccessException("Wrong client credential");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
} catch (final Exception e) {
|
} catch (final Exception e) {
|
||||||
this.credentials = null;
|
log.info("Authentication failed: ", e);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,25 +22,37 @@ import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.http.HttpEntity;
|
import org.springframework.http.HttpEntity;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.HttpMethod;
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||||
|
import org.springframework.security.access.AccessDeniedException;
|
||||||
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
|
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
|
||||||
|
import org.springframework.security.oauth2.client.resource.OAuth2AccessDeniedException;
|
||||||
|
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
|
||||||
|
import org.springframework.security.oauth2.client.resource.UserRedirectRequiredException;
|
||||||
|
import org.springframework.security.oauth2.client.token.AccessTokenRequest;
|
||||||
|
import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsAccessTokenProvider;
|
||||||
import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails;
|
import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails;
|
||||||
import org.springframework.security.oauth2.common.OAuth2AccessToken;
|
import org.springframework.security.oauth2.common.OAuth2AccessToken;
|
||||||
|
import org.springframework.util.LinkedMultiValueMap;
|
||||||
|
import org.springframework.util.MultiValueMap;
|
||||||
|
|
||||||
import ch.ethz.seb.sebserver.gbl.model.Domain.LMS_SETUP;
|
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.Page;
|
|
||||||
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
|
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
|
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
|
|
||||||
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
|
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails;
|
import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails;
|
||||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.util.SupplierWithCircuitBreaker;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentialService;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentialService;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentials;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentials;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.LmsSetupDAO;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
|
||||||
|
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
|
||||||
|
|
||||||
|
/** Implements the LmsAPITemplate for Open edX LMS Course API access.
|
||||||
|
*
|
||||||
|
* See also: https://course-catalog-api-guide.readthedocs.io */
|
||||||
final class OpenEdxLmsAPITemplate implements LmsAPITemplate {
|
final class OpenEdxLmsAPITemplate implements LmsAPITemplate {
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(OpenEdxLmsAPITemplate.class);
|
private static final Logger log = LoggerFactory.getLogger(OpenEdxLmsAPITemplate.class);
|
||||||
|
@ -49,26 +61,26 @@ final class OpenEdxLmsAPITemplate implements LmsAPITemplate {
|
||||||
private static final String OPEN_EDX_DEFAULT_COURSE_ENDPOINT = "/api/courses/v1/courses/";
|
private static final String OPEN_EDX_DEFAULT_COURSE_ENDPOINT = "/api/courses/v1/courses/";
|
||||||
private static final String OPEN_EDX_DEFAULT_COURSE_START_URL_PREFIX = "/courses/";
|
private static final String OPEN_EDX_DEFAULT_COURSE_START_URL_PREFIX = "/courses/";
|
||||||
|
|
||||||
private final String lmsSetupId;
|
private final LmsSetup lmsSetup;
|
||||||
private final LmsSetupDAO lmsSetupDAO;
|
private final ClientCredentials credentials;
|
||||||
private final ClientHttpRequestFactory clientHttpRequestFactory;
|
private final ClientHttpRequestFactory clientHttpRequestFactory;
|
||||||
private final ClientCredentialService clientCredentialService;
|
private final ClientCredentialService clientCredentialService;
|
||||||
private final Set<String> knownTokenAccessPaths;
|
private final Set<String> knownTokenAccessPaths;
|
||||||
|
|
||||||
private OAuth2RestTemplate restTemplate = null;
|
private OAuth2RestTemplate restTemplate = null;
|
||||||
|
private SupplierWithCircuitBreaker<List<QuizData>> allQuizzesSupplier = null;
|
||||||
|
|
||||||
OpenEdxLmsAPITemplate(
|
OpenEdxLmsAPITemplate(
|
||||||
final String lmsSetupId,
|
final LmsSetup lmsSetup,
|
||||||
final LmsSetupDAO lmsSetupDAO,
|
final ClientCredentials credentials,
|
||||||
final ClientCredentialService clientCredentialService,
|
final ClientCredentialService clientCredentialService,
|
||||||
final ClientHttpRequestFactory clientHttpRequestFactory,
|
final ClientHttpRequestFactory clientHttpRequestFactory,
|
||||||
final String[] alternativeTokenRequestPaths) {
|
final String[] alternativeTokenRequestPaths) {
|
||||||
|
|
||||||
this.lmsSetupId = lmsSetupId;
|
this.lmsSetup = lmsSetup;
|
||||||
this.lmsSetupDAO = lmsSetupDAO;
|
|
||||||
this.clientHttpRequestFactory = clientHttpRequestFactory;
|
|
||||||
this.clientCredentialService = clientCredentialService;
|
this.clientCredentialService = clientCredentialService;
|
||||||
|
this.credentials = credentials;
|
||||||
|
this.clientHttpRequestFactory = clientHttpRequestFactory;
|
||||||
this.knownTokenAccessPaths = new HashSet<>();
|
this.knownTokenAccessPaths = new HashSet<>();
|
||||||
this.knownTokenAccessPaths.add(OPEN_EDX_DEFAULT_TOKEN_REQUEST_PATH);
|
this.knownTokenAccessPaths.add(OPEN_EDX_DEFAULT_TOKEN_REQUEST_PATH);
|
||||||
if (alternativeTokenRequestPaths != null) {
|
if (alternativeTokenRequestPaths != null) {
|
||||||
|
@ -77,87 +89,45 @@ final class OpenEdxLmsAPITemplate implements LmsAPITemplate {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Result<LmsSetup> lmsSetup() {
|
public LmsSetup lmsSetup() {
|
||||||
return this.lmsSetupDAO
|
return this.lmsSetup;
|
||||||
.byModelId(this.lmsSetupId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public LmsSetupTestResult testLmsSetup() {
|
public LmsSetupTestResult testLmsSetup() {
|
||||||
|
|
||||||
final LmsSetup lmsSetup = lmsSetup().getOrThrow();
|
log.info("Test Lms Binding for OpenEdX and LmsSetup: {}", this.lmsSetup);
|
||||||
|
|
||||||
log.info("Test Lms Binding for OpenEdX and LmsSetup: {}", lmsSetup);
|
|
||||||
|
|
||||||
// validation of LmsSetup
|
|
||||||
if (lmsSetup.lmsType != LmsType.MOCKUP) {
|
|
||||||
return LmsSetupTestResult.ofMissingAttributes(LMS_SETUP.ATTR_LMS_TYPE);
|
|
||||||
}
|
|
||||||
final List<String> missingAttrs = new ArrayList<>();
|
|
||||||
if (StringUtils.isBlank(lmsSetup.lmsApiUrl)) {
|
|
||||||
missingAttrs.add(LMS_SETUP.ATTR_LMS_TYPE);
|
|
||||||
}
|
|
||||||
if (StringUtils.isBlank(lmsSetup.getLmsAuthName())) {
|
|
||||||
missingAttrs.add(LMS_SETUP.ATTR_LMS_CLIENTNAME);
|
|
||||||
}
|
|
||||||
if (StringUtils.isBlank(lmsSetup.getLmsAuthSecret())) {
|
|
||||||
missingAttrs.add(LMS_SETUP.ATTR_LMS_CLIENTSECRET);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
final List<APIMessage> missingAttrs = attributeValidation(this.credentials);
|
||||||
if (!missingAttrs.isEmpty()) {
|
if (!missingAttrs.isEmpty()) {
|
||||||
return LmsSetupTestResult.ofMissingAttributes(missingAttrs);
|
return LmsSetupTestResult.ofMissingAttributes(missingAttrs);
|
||||||
}
|
}
|
||||||
|
|
||||||
// request OAuth2 access token on OpenEdx API
|
// request OAuth2 access token on OpenEdx API
|
||||||
initRestTemplateAndRequestAccessToken(lmsSetup);
|
initRestTemplateAndRequestAccessToken();
|
||||||
if (this.restTemplate == null) {
|
if (this.restTemplate == null) {
|
||||||
return LmsSetupTestResult.ofTokenRequestError(
|
return LmsSetupTestResult.ofTokenRequestError(
|
||||||
"Failed to gain access token form OpenEdX Rest API: tried token endpoints: " +
|
"Failed to gain access token from OpenEdX Rest API:\n tried token endpoints: " +
|
||||||
this.knownTokenAccessPaths);
|
this.knownTokenAccessPaths);
|
||||||
}
|
}
|
||||||
|
|
||||||
// query quizzes TODO!?
|
|
||||||
|
|
||||||
return LmsSetupTestResult.ofOkay();
|
return LmsSetupTestResult.ofOkay();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Result<Page<QuizData>> getQuizzes(
|
public Result<List<QuizData>> getQuizzes(final FilterMap filterMap) {
|
||||||
final String name,
|
return this.initRestTemplateAndRequestAccessToken()
|
||||||
final Long from,
|
.flatMap(this::getAllQuizes)
|
||||||
final String sort,
|
.map(LmsAPIService.quizzesFilterFunction(filterMap));
|
||||||
final int pageNumber,
|
}
|
||||||
final int pageSize) {
|
|
||||||
|
|
||||||
return this.lmsSetup()
|
public ResponseEntity<EdXPage> getEdxPage(final String pageURI) {
|
||||||
.flatMap(this::initRestTemplateAndRequestAccessToken)
|
final HttpHeaders httpHeaders = new HttpHeaders();
|
||||||
.map(lmsSetup -> {
|
return this.restTemplate.exchange(
|
||||||
|
pageURI,
|
||||||
// TODO sort and pagination
|
HttpMethod.GET,
|
||||||
final HttpHeaders httpHeaders = new HttpHeaders();
|
new HttpEntity<>(httpHeaders),
|
||||||
|
EdXPage.class);
|
||||||
final ResponseEntity<EdXPage> response = this.restTemplate.exchange(
|
|
||||||
lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_ENDPOINT,
|
|
||||||
HttpMethod.GET,
|
|
||||||
new HttpEntity<>(httpHeaders),
|
|
||||||
EdXPage.class);
|
|
||||||
final EdXPage edxpage = response.getBody();
|
|
||||||
|
|
||||||
final List<QuizData> content = edxpage.results
|
|
||||||
.stream()
|
|
||||||
.reduce(
|
|
||||||
new ArrayList<QuizData>(),
|
|
||||||
(list, courseData) -> {
|
|
||||||
list.add(quizDataOf(lmsSetup, courseData));
|
|
||||||
return list;
|
|
||||||
},
|
|
||||||
(list1, list2) -> {
|
|
||||||
list1.addAll(list2);
|
|
||||||
return list1;
|
|
||||||
});
|
|
||||||
|
|
||||||
return new Page<>(edxpage.num_pages, pageNumber, sort, content);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -172,20 +142,15 @@ final class OpenEdxLmsAPITemplate implements LmsAPITemplate {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private Result<LmsSetup> initRestTemplateAndRequestAccessToken() {
|
||||||
public void reset() {
|
|
||||||
this.restTemplate = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Result<LmsSetup> initRestTemplateAndRequestAccessToken(final LmsSetup lmsSetup) {
|
log.info("Initialize Rest Template for OpenEdX API access. LmsSetup: {}", this.lmsSetup);
|
||||||
|
|
||||||
log.info("Initialize Rest Template for OpenEdX API access. LmsSetup: {}", lmsSetup);
|
|
||||||
|
|
||||||
return Result.tryCatch(() -> {
|
return Result.tryCatch(() -> {
|
||||||
if (this.restTemplate != null) {
|
if (this.restTemplate != null) {
|
||||||
try {
|
try {
|
||||||
this.restTemplate.getAccessToken();
|
this.restTemplate.getAccessToken();
|
||||||
return lmsSetup;
|
return this.lmsSetup;
|
||||||
} catch (final Exception e) {
|
} catch (final Exception e) {
|
||||||
log.warn(
|
log.warn(
|
||||||
"Error while trying to get access token within already existing OAuth2RestTemplate instance. Try to create new one.",
|
"Error while trying to get access token within already existing OAuth2RestTemplate instance. Try to create new one.",
|
||||||
|
@ -194,24 +159,20 @@ final class OpenEdxLmsAPITemplate implements LmsAPITemplate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final ClientCredentials credentials = this.lmsSetupDAO
|
|
||||||
.getLmsAPIAccessCredentials(this.lmsSetupId)
|
|
||||||
.getOrThrow();
|
|
||||||
|
|
||||||
final Iterator<String> tokenAccessPaths = this.knownTokenAccessPaths.iterator();
|
final Iterator<String> tokenAccessPaths = this.knownTokenAccessPaths.iterator();
|
||||||
while (tokenAccessPaths.hasNext()) {
|
while (tokenAccessPaths.hasNext()) {
|
||||||
final String accessTokenRequestPath = tokenAccessPaths.next();
|
final String accessTokenRequestPath = tokenAccessPaths.next();
|
||||||
try {
|
try {
|
||||||
|
|
||||||
final OAuth2RestTemplate template = createRestTemplate(
|
final OAuth2RestTemplate template = createRestTemplate(
|
||||||
lmsSetup,
|
this.lmsSetup,
|
||||||
credentials,
|
this.credentials,
|
||||||
accessTokenRequestPath);
|
accessTokenRequestPath);
|
||||||
|
|
||||||
final OAuth2AccessToken accessToken = template.getAccessToken();
|
final OAuth2AccessToken accessToken = template.getAccessToken();
|
||||||
if (accessToken != null) {
|
if (accessToken != null) {
|
||||||
this.restTemplate = template;
|
this.restTemplate = template;
|
||||||
return lmsSetup;
|
return this.lmsSetup;
|
||||||
}
|
}
|
||||||
} catch (final Exception e) {
|
} catch (final Exception e) {
|
||||||
log.info("Failed to request access token on access token request path: {}", accessTokenRequestPath,
|
log.info("Failed to request access token on access token request path: {}", accessTokenRequestPath,
|
||||||
|
@ -219,7 +180,8 @@ final class OpenEdxLmsAPITemplate implements LmsAPITemplate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new IllegalArgumentException("Unable to establish OpenEdX API connection for lmsSetup: " + lmsSetup);
|
throw new IllegalArgumentException(
|
||||||
|
"Unable to establish OpenEdX API connection for lmsSetup: " + this.lmsSetup);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -235,17 +197,49 @@ final class OpenEdxLmsAPITemplate implements LmsAPITemplate {
|
||||||
details.setAccessTokenUri(lmsSetup.lmsApiUrl + accessTokenRequestPath);
|
details.setAccessTokenUri(lmsSetup.lmsApiUrl + accessTokenRequestPath);
|
||||||
details.setClientId(plainClientId.toString());
|
details.setClientId(plainClientId.toString());
|
||||||
details.setClientSecret(plainClientSecret.toString());
|
details.setClientSecret(plainClientSecret.toString());
|
||||||
details.setGrantType("client_credentials");
|
|
||||||
|
|
||||||
// TODO: accordingly to the documentation (https://course-catalog-api-guide.readthedocs.io/en/latest/authentication/#create-an-account-on-edx-org-for-api-access)
|
|
||||||
// token_type=jwt is needed for token request but is it possible to set this within ClientCredentialsResourceDetails
|
|
||||||
// or within the request header on API call. To clarify
|
|
||||||
|
|
||||||
final OAuth2RestTemplate template = new OAuth2RestTemplate(details);
|
final OAuth2RestTemplate template = new OAuth2RestTemplate(details);
|
||||||
template.setRequestFactory(this.clientHttpRequestFactory);
|
template.setRequestFactory(this.clientHttpRequestFactory);
|
||||||
|
template.setAccessTokenProvider(new EdxClientCredentialsAccessTokenProvider());
|
||||||
return template;
|
return template;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Result<List<QuizData>> getAllQuizes(final LmsSetup lmsSetup) {
|
||||||
|
if (this.allQuizzesSupplier == null) {
|
||||||
|
this.allQuizzesSupplier = new SupplierWithCircuitBreaker<>(
|
||||||
|
() -> collectAllCourses(lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_ENDPOINT)
|
||||||
|
.stream()
|
||||||
|
.reduce(
|
||||||
|
new ArrayList<QuizData>(),
|
||||||
|
(list, courseData) -> {
|
||||||
|
list.add(quizDataOf(lmsSetup, courseData));
|
||||||
|
return list;
|
||||||
|
},
|
||||||
|
(list1, list2) -> {
|
||||||
|
list1.addAll(list2);
|
||||||
|
return list1;
|
||||||
|
}),
|
||||||
|
5, 1000L); // TODO specify better CircuitBreaker params
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.allQuizzesSupplier.get();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<CourseData> collectAllCourses(final String pageURI) {
|
||||||
|
final List<CourseData> collector = new ArrayList<>();
|
||||||
|
EdXPage page = getEdxPage(pageURI).getBody();
|
||||||
|
if (page != null) {
|
||||||
|
collector.addAll(page.results);
|
||||||
|
while (StringUtils.isNoneBlank(page.next)) {
|
||||||
|
page = getEdxPage(page.next).getBody();
|
||||||
|
collector.addAll(page.results);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return collector;
|
||||||
|
}
|
||||||
|
|
||||||
private QuizData quizDataOf(
|
private QuizData quizDataOf(
|
||||||
final LmsSetup lmsSetup,
|
final LmsSetup lmsSetup,
|
||||||
final CourseData courseData) {
|
final CourseData courseData) {
|
||||||
|
@ -260,14 +254,16 @@ final class OpenEdxLmsAPITemplate implements LmsAPITemplate {
|
||||||
startURI);
|
startURI);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Maps a OpenEdX course API course page */
|
||||||
static final class EdXPage {
|
static final class EdXPage {
|
||||||
public Integer count;
|
public Integer count;
|
||||||
public Integer previous;
|
public String previous;
|
||||||
public Integer num_pages;
|
public Integer num_pages;
|
||||||
public Integer next;
|
public String next;
|
||||||
public List<CourseData> results;
|
public List<CourseData> results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Maps the OpenEdX course API course data */
|
||||||
static final class CourseData {
|
static final class CourseData {
|
||||||
public String id;
|
public String id;
|
||||||
public String course_id;
|
public String course_id;
|
||||||
|
@ -278,41 +274,31 @@ final class OpenEdxLmsAPITemplate implements LmsAPITemplate {
|
||||||
public String end;
|
public String end;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/** A custom ClientCredentialsAccessTokenProvider that adapts the access token request to Open edX
|
||||||
* pagination
|
* access token request protocol using a form-URL-encoded POST request according to:
|
||||||
* count 2
|
* https://course-catalog-api-guide.readthedocs.io/en/latest/authentication/index.html#getting-an-access-token */
|
||||||
* previous null
|
private class EdxClientCredentialsAccessTokenProvider extends ClientCredentialsAccessTokenProvider {
|
||||||
* num_pages 1
|
|
||||||
* next null
|
@Override
|
||||||
* results
|
public OAuth2AccessToken obtainAccessToken(
|
||||||
* 0
|
final OAuth2ProtectedResourceDetails details,
|
||||||
* blocks_url "http://ralph.ethz.ch:18000/api/courses/v1/blocks/?course_id=course-v1%3AedX%2BDemoX%2BDemo_Course"
|
final AccessTokenRequest request)
|
||||||
* effort null
|
throws UserRedirectRequiredException,
|
||||||
* end null
|
AccessDeniedException,
|
||||||
* enrollment_start null
|
OAuth2AccessDeniedException {
|
||||||
* enrollment_end null
|
|
||||||
* id "course-v1:edX+DemoX+Demo_Course"
|
final ClientCredentialsResourceDetails resource = (ClientCredentialsResourceDetails) details;
|
||||||
* media
|
final HttpHeaders headers = new HttpHeaders();
|
||||||
* course_image
|
headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE);
|
||||||
* uri "/asset-v1:edX+DemoX+Demo_Course+type@asset+block@images_course_image.jpg"
|
|
||||||
* course_video
|
final MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
|
||||||
* uri null
|
params.add("grant_type", "client_credentials");
|
||||||
* image
|
params.add("token_type", "jwt");
|
||||||
* raw "http://ralph.ethz.ch:18000/asset-v1:edX+DemoX+Demo_Course+type@asset+block@images_course_image.jpg"
|
params.add("client_id", resource.getClientId());
|
||||||
* small "http://ralph.ethz.ch:18000/asset-v1:edX+DemoX+Demo_Course+type@asset+block@images_course_image.jpg"
|
params.add("client_secret", resource.getClientSecret());
|
||||||
* large "http://ralph.ethz.ch:18000/asset-v1:edX+DemoX+Demo_Course+type@asset+block@images_course_image.jpg"
|
|
||||||
* name "edX Demonstration Course"
|
return retrieveToken(request, resource, params, headers);
|
||||||
* number "DemoX"
|
}
|
||||||
* org "edX"
|
}
|
||||||
* short_description null
|
|
||||||
* start "2013-02-05T05:00:00Z"
|
|
||||||
* start_display "Feb. 5, 2013"
|
|
||||||
* start_type "timestamp"
|
|
||||||
* pacing "instructor"
|
|
||||||
* mobile_available false
|
|
||||||
* hidden false
|
|
||||||
* invitation_only false
|
|
||||||
* course_id "course-v1:edX+DemoX+Demo_Course"
|
|
||||||
*/
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -137,7 +137,7 @@ public class APIExceptionHandler extends ResponseEntityExceptionHandler {
|
||||||
final WebRequest request) {
|
final WebRequest request) {
|
||||||
|
|
||||||
return new ResponseEntity<>(
|
return new ResponseEntity<>(
|
||||||
Arrays.asList(ex.getAPIMessage()),
|
ex.getAPIMessages(),
|
||||||
HttpStatus.BAD_REQUEST);
|
HttpStatus.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -253,7 +253,7 @@ public class ExamAdministrationController extends ActivatableEntityController<Ex
|
||||||
final String quizId = postParams.getString(QuizData.QUIZ_ATTR_ID);
|
final String quizId = postParams.getString(QuizData.QUIZ_ATTR_ID);
|
||||||
|
|
||||||
final LmsAPITemplate lmsAPITemplate = this.lmsAPIService
|
final LmsAPITemplate lmsAPITemplate = this.lmsAPIService
|
||||||
.createLmsAPITemplate(lmsSetupId)
|
.getLmsAPITemplate(lmsSetupId)
|
||||||
.getOrThrow();
|
.getOrThrow();
|
||||||
|
|
||||||
final QuizData quiz = lmsAPITemplate.getQuizzes(new HashSet<>(Arrays.asList(quizId)))
|
final QuizData quiz = lmsAPITemplate.getQuizzes(new HashSet<>(Arrays.asList(quizId)))
|
||||||
|
|
|
@ -16,6 +16,7 @@ import org.springframework.web.bind.annotation.RequestMethod;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
import ch.ethz.seb.sebserver.gbl.api.API;
|
import ch.ethz.seb.sebserver.gbl.api.API;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.api.APIMessage.APIMessageException;
|
||||||
import ch.ethz.seb.sebserver.gbl.api.EntityType;
|
import ch.ethz.seb.sebserver.gbl.api.EntityType;
|
||||||
import ch.ethz.seb.sebserver.gbl.api.POSTMapper;
|
import ch.ethz.seb.sebserver.gbl.api.POSTMapper;
|
||||||
import ch.ethz.seb.sebserver.gbl.authorization.PrivilegeType;
|
import ch.ethz.seb.sebserver.gbl.authorization.PrivilegeType;
|
||||||
|
@ -75,9 +76,15 @@ public class LmsSetupController extends ActivatableEntityController<LmsSetup, Lm
|
||||||
|
|
||||||
this.authorization.check(PrivilegeType.MODIFY, EntityType.LMS_SETUP);
|
this.authorization.check(PrivilegeType.MODIFY, EntityType.LMS_SETUP);
|
||||||
|
|
||||||
return this.lmsAPIService.createLmsAPITemplate(modelId)
|
final LmsSetupTestResult result = this.lmsAPIService.getLmsAPITemplate(modelId)
|
||||||
.map(template -> template.testLmsSetup())
|
.map(template -> template.testLmsSetup())
|
||||||
.getOrThrow();
|
.getOrThrow();
|
||||||
|
|
||||||
|
if (result.missingLMSSetupAttribute != null && !result.missingLMSSetupAttribute.isEmpty()) {
|
||||||
|
throw new APIMessageException(result.missingLMSSetupAttribute);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
package ch.ethz.seb.sebserver.webservice.weblayer.api;
|
package ch.ethz.seb.sebserver.webservice.weblayer.api;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.util.MultiValueMap;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMethod;
|
import org.springframework.web.bind.annotation.RequestMethod;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
@ -17,16 +18,14 @@ import org.springframework.web.bind.annotation.RestController;
|
||||||
import ch.ethz.seb.sebserver.gbl.api.API;
|
import ch.ethz.seb.sebserver.gbl.api.API;
|
||||||
import ch.ethz.seb.sebserver.gbl.api.EntityType;
|
import ch.ethz.seb.sebserver.gbl.api.EntityType;
|
||||||
import ch.ethz.seb.sebserver.gbl.authorization.PrivilegeType;
|
import ch.ethz.seb.sebserver.gbl.authorization.PrivilegeType;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.Domain.LMS_SETUP;
|
|
||||||
import ch.ethz.seb.sebserver.gbl.model.Entity;
|
import ch.ethz.seb.sebserver.gbl.model.Entity;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.Page;
|
import ch.ethz.seb.sebserver.gbl.model.Page;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
|
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
|
||||||
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
||||||
import ch.ethz.seb.sebserver.gbl.util.Utils;
|
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.AuthorizationService;
|
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.UserService;
|
||||||
|
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
|
|
||||||
|
|
||||||
@WebServiceProfile
|
@WebServiceProfile
|
||||||
@RestController
|
@RestController
|
||||||
|
@ -57,26 +56,20 @@ public class QuizImportController {
|
||||||
name = Entity.FILTER_ATTR_INSTITUTION,
|
name = Entity.FILTER_ATTR_INSTITUTION,
|
||||||
required = true,
|
required = true,
|
||||||
defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId,
|
defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId,
|
||||||
@RequestParam(name = LMS_SETUP.ATTR_ID, required = true) final Long lmsSetupId,
|
|
||||||
@RequestParam(name = QuizData.FILTER_ATTR_NAME, required = false) final String nameLike,
|
|
||||||
@RequestParam(name = QuizData.FILTER_ATTR_START_TIME, required = false) final String startTime,
|
|
||||||
@RequestParam(name = Page.ATTR_PAGE_NUMBER, required = false) final Integer pageNumber,
|
@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_PAGE_SIZE, required = false) final Integer pageSize,
|
||||||
@RequestParam(name = Page.ATTR_SORT, required = false) final String sort) {
|
@RequestParam(name = Page.ATTR_SORT, required = false) final String sort,
|
||||||
|
@RequestParam final MultiValueMap<String, String> allRequestParams) {
|
||||||
final LmsAPITemplate lmsAPITemplate = this.lmsAPIService
|
|
||||||
.createLmsAPITemplate(lmsSetupId)
|
|
||||||
.getOrThrow();
|
|
||||||
|
|
||||||
this.authorization.check(
|
this.authorization.check(
|
||||||
PrivilegeType.READ_ONLY,
|
PrivilegeType.READ_ONLY,
|
||||||
EntityType.EXAM,
|
EntityType.EXAM,
|
||||||
institutionId);
|
institutionId);
|
||||||
|
|
||||||
return lmsAPITemplate.getQuizzes(
|
final FilterMap filterMap = new FilterMap(allRequestParams);
|
||||||
nameLike,
|
filterMap.putIfAbsent(Entity.FILTER_ATTR_INSTITUTION, String.valueOf(institutionId));
|
||||||
Utils.dateTimeStringToTimestamp(startTime, null),
|
|
||||||
sort,
|
return this.lmsAPIService.requestQuizDataPage(
|
||||||
(pageNumber != null)
|
(pageNumber != null)
|
||||||
? pageNumber
|
? pageNumber
|
||||||
: 1,
|
: 1,
|
||||||
|
@ -84,7 +77,9 @@ public class QuizImportController {
|
||||||
? (pageSize <= this.maxPageSize)
|
? (pageSize <= this.maxPageSize)
|
||||||
? pageSize
|
? pageSize
|
||||||
: this.maxPageSize
|
: this.maxPageSize
|
||||||
: this.defaultPageSize)
|
: this.defaultPageSize,
|
||||||
|
sort,
|
||||||
|
filterMap)
|
||||||
.getOrThrow();
|
.getOrThrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,7 @@ sebserver.form.validation.fieldError.notNull=This field is mandatory
|
||||||
sebserver.form.validation.fieldError.username.notunique=This Username is already in use. Please choose another one.
|
sebserver.form.validation.fieldError.username.notunique=This Username is already in use. Please choose another one.
|
||||||
sebserver.form.validation.fieldError.password.wrong=Old password is wrong
|
sebserver.form.validation.fieldError.password.wrong=Old password is wrong
|
||||||
sebserver.form.validation.fieldError.password.mismatch=Re-typed password don't match new password
|
sebserver.form.validation.fieldError.password.mismatch=Re-typed password don't match new password
|
||||||
|
sebserver.form.validation.fieldError.invalidURL=The input does not match the URL pattern.
|
||||||
sebserver.error.unexpected=Unexpected Error
|
sebserver.error.unexpected=Unexpected Error
|
||||||
sebserver.page.message=Information
|
sebserver.page.message=Information
|
||||||
sebserver.dialog.confirm.title=Confirmation
|
sebserver.dialog.confirm.title=Confirmation
|
||||||
|
@ -159,6 +160,12 @@ sebserver.lmssetup.action.new=New LMS Setup
|
||||||
sebserver.lmssetup.action.list.view=View Selected
|
sebserver.lmssetup.action.list.view=View Selected
|
||||||
sebserver.lmssetup.action.list.modify=Edit Selected
|
sebserver.lmssetup.action.list.modify=Edit Selected
|
||||||
sebserver.lmssetup.action.modify=Edit
|
sebserver.lmssetup.action.modify=Edit
|
||||||
|
sebserver.lmssetup.action.test=Test Setup
|
||||||
|
sebserver.lmssetup.action.test.ok=Successfully connect to the LMSs course API
|
||||||
|
sebserver.lmssetup.action.test.tokenRequestError=The API access was denied: {0}
|
||||||
|
sebserver.lmssetup.action.test.quizRequestError=Unable to request courses or quizzes from the course API of the LMS. {0}
|
||||||
|
sebserver.lmssetup.action.test.missingParameter=There is one or more missing connection parameter.<br/>Please check the connection parameter for this LMS Setup
|
||||||
|
sebserver.lmssetup.action.test.unknownError=An unexpected error happened while trying to connect to the LMS course API. {0}
|
||||||
sebserver.lmssetup.action.save=Save LMS Setup
|
sebserver.lmssetup.action.save=Save LMS Setup
|
||||||
sebserver.lmssetup.action.activate=Active
|
sebserver.lmssetup.action.activate=Active
|
||||||
sebserver.lmssetup.action.deactivate=Active
|
sebserver.lmssetup.action.deactivate=Active
|
||||||
|
|
|
@ -3,10 +3,13 @@ SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0;
|
||||||
SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0;
|
SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0;
|
||||||
SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='TRADITIONAL,ALLOW_INVALID_DATES';
|
SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='TRADITIONAL,ALLOW_INVALID_DATES';
|
||||||
|
|
||||||
|
|
||||||
-- -----------------------------------------------------
|
-- -----------------------------------------------------
|
||||||
-- Schema SEBServer
|
-- Schema SEBServer
|
||||||
-- -----------------------------------------------------
|
-- -----------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- -----------------------------------------------------
|
-- -----------------------------------------------------
|
||||||
-- Table `institution`
|
-- Table `institution`
|
||||||
-- -----------------------------------------------------
|
-- -----------------------------------------------------
|
||||||
|
|
BIN
src/main/resources/static/images/test.png
Normal file
BIN
src/main/resources/static/images/test.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 146 B |
|
@ -32,23 +32,23 @@ public class ClientCredentialServiceTest {
|
||||||
@Test
|
@Test
|
||||||
public void testEncryptDecryptClientCredentials() {
|
public void testEncryptDecryptClientCredentials() {
|
||||||
final Environment envMock = mock(Environment.class);
|
final Environment envMock = mock(Environment.class);
|
||||||
when(envMock.getRequiredProperty(ClientCredentialService.SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY))
|
when(envMock.getRequiredProperty(ClientCredentialServiceImpl.SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY))
|
||||||
.thenReturn("secret1");
|
.thenReturn("secret1");
|
||||||
|
|
||||||
final String clientName = "simpleClientName";
|
final String clientName = "simpleClientName";
|
||||||
|
|
||||||
final ClientCredentialService service = new ClientCredentialService(envMock);
|
final ClientCredentialServiceImpl service = new ClientCredentialServiceImpl(envMock);
|
||||||
String encrypted =
|
String encrypted =
|
||||||
service.encrypt(clientName, "secret1", ClientCredentialService.DEFAULT_SALT).toString();
|
service.encrypt(clientName, "secret1", ClientCredentialServiceImpl.DEFAULT_SALT).toString();
|
||||||
String decrypted = service.decrypt(encrypted, "secret1", ClientCredentialService.DEFAULT_SALT).toString();
|
String decrypted = service.decrypt(encrypted, "secret1", ClientCredentialServiceImpl.DEFAULT_SALT).toString();
|
||||||
|
|
||||||
assertEquals(clientName, decrypted);
|
assertEquals(clientName, decrypted);
|
||||||
|
|
||||||
final String clientSecret = "fbjreij39ru29305ruࣣàèLöäöäü65%(/%(ç87";
|
final String clientSecret = "fbjreij39ru29305ruࣣàèLöäöäü65%(/%(ç87";
|
||||||
|
|
||||||
encrypted =
|
encrypted =
|
||||||
service.encrypt(clientSecret, "secret1", ClientCredentialService.DEFAULT_SALT).toString();
|
service.encrypt(clientSecret, "secret1", ClientCredentialServiceImpl.DEFAULT_SALT).toString();
|
||||||
decrypted = service.decrypt(encrypted, "secret1", ClientCredentialService.DEFAULT_SALT).toString();
|
decrypted = service.decrypt(encrypted, "secret1", ClientCredentialServiceImpl.DEFAULT_SALT).toString();
|
||||||
|
|
||||||
assertEquals(clientSecret, decrypted);
|
assertEquals(clientSecret, decrypted);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue