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 */
|
||||
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 String LIST_SEPARATOR = ",";
|
||||
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_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";
|
||||
|
||||
|
|
|
@ -169,25 +169,30 @@ public class APIMessage implements Serializable {
|
|||
|
||||
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) {
|
||||
super();
|
||||
this.apiMessage = apiMessage;
|
||||
this.apiMessages = Arrays.asList(apiMessage);
|
||||
}
|
||||
|
||||
public APIMessageException(final ErrorMessage errorMessage) {
|
||||
super();
|
||||
this.apiMessage = errorMessage.of();
|
||||
this.apiMessages = Arrays.asList(errorMessage.of());
|
||||
}
|
||||
|
||||
public APIMessageException(final ErrorMessage errorMessage, final String detail, final String... attributes) {
|
||||
super();
|
||||
this.apiMessage = errorMessage.of(detail, attributes);
|
||||
this.apiMessages = Arrays.asList(errorMessage.of(detail, attributes));
|
||||
}
|
||||
|
||||
public APIMessage getAPIMessage() {
|
||||
return this.apiMessage;
|
||||
public Collection<APIMessage> getAPIMessages() {
|
||||
return this.apiMessages;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -27,8 +27,6 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.GrantEntity;
|
|||
|
||||
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_TYPE = "type";
|
||||
public static final String FILTER_ATTR_FROM = "from";
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
|
||||
package ch.ethz.seb.sebserver.gbl.model.exam;
|
||||
|
||||
import java.util.Comparator;
|
||||
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.DateTimeZone;
|
||||
import org.joda.time.LocalDateTime;
|
||||
|
@ -16,10 +18,12 @@ import com.fasterxml.jackson.annotation.JsonCreator;
|
|||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.Constants;
|
||||
import ch.ethz.seb.sebserver.gbl.api.EntityType;
|
||||
import ch.ethz.seb.sebserver.gbl.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 QUIZ_ATTR_ID = "quiz_id";
|
||||
|
@ -84,10 +88,25 @@ public final class QuizData {
|
|||
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() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
@ -140,4 +159,62 @@ public final class QuizData {
|
|||
+ ", 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.Size;
|
||||
|
||||
import org.hibernate.validator.constraints.URL;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
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 static final String FILTER_ATTR_LMS_SETUP = "lms_setup";
|
||||
public static final String FILTER_ATTR_LMS_TYPE = "lms_type";
|
||||
|
||||
public enum LmsType {
|
||||
MOCKUP,
|
||||
MOODLE,
|
||||
OPEN_EDX
|
||||
}
|
||||
|
||||
|
@ -51,14 +53,13 @@ public final class LmsSetup implements GrantEntity, Activatable {
|
|||
public final LmsType lmsType;
|
||||
|
||||
@JsonProperty(LMS_SETUP.ATTR_LMS_CLIENTNAME)
|
||||
@Size(min = 3, max = 255, message = "lmsSetup:lmsClientname:size:{min}:{max}:${validatedValue}")
|
||||
public final String lmsAuthName;
|
||||
|
||||
@JsonProperty(LMS_SETUP.ATTR_LMS_CLIENTSECRET)
|
||||
@Size(min = 8, max = 255, message = "lmsSetup:lmsClientsecret:size:{min}:{max}:${validatedValue}")
|
||||
public final String lmsAuthSecret;
|
||||
|
||||
@JsonProperty(LMS_SETUP.ATTR_LMS_URL)
|
||||
@URL(message = "lmsSetup:lmsUrl:invalidURL")
|
||||
public final String lmsApiUrl;
|
||||
|
||||
@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.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import java.util.List;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
|
||||
import ch.ethz.seb.sebserver.gbl.util.Utils;
|
||||
|
||||
public final class LmsSetupTestResult {
|
||||
|
@ -31,32 +33,37 @@ public final class LmsSetupTestResult {
|
|||
public final Boolean okStatus;
|
||||
|
||||
@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;
|
||||
|
||||
@JsonProperty(ATTR_MISSING_ATTRIBUTE)
|
||||
@JsonProperty(ATTR_ERROR_QUIZ_REQUEST)
|
||||
public final String quizRequestError;
|
||||
|
||||
public LmsSetupTestResult(
|
||||
@JsonProperty(value = ATTR_OK_STATUS, required = true) final Boolean ok,
|
||||
@JsonProperty(ATTR_MISSING_ATTRIBUTE) final Collection<String> missingLMSSetupAttribute,
|
||||
@JsonProperty(ATTR_MISSING_ATTRIBUTE) final String tokenRequestError,
|
||||
@JsonProperty(ATTR_MISSING_ATTRIBUTE) final String quizRequestError) {
|
||||
@JsonProperty(ATTR_MISSING_ATTRIBUTE) final Collection<APIMessage> missingLMSSetupAttribute,
|
||||
@JsonProperty(ATTR_ERROR_TOKEN_REQUEST) final String tokenRequestError,
|
||||
@JsonProperty(ATTR_ERROR_QUIZ_REQUEST) final String quizRequestError) {
|
||||
|
||||
this.okStatus = ok;
|
||||
// TODO
|
||||
this.missingLMSSetupAttribute = Utils.immutableSetOf(missingLMSSetupAttribute);
|
||||
this.missingLMSSetupAttribute = Utils.immutableListOf(missingLMSSetupAttribute);
|
||||
this.tokenRequestError = tokenRequestError;
|
||||
this.quizRequestError = quizRequestError;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public boolean isOk() {
|
||||
return this.okStatus != null && this.okStatus.booleanValue();
|
||||
}
|
||||
|
||||
public Boolean getOkStatus() {
|
||||
return this.okStatus;
|
||||
}
|
||||
|
||||
public Set<String> getMissingLMSSetupAttribute() {
|
||||
public List<APIMessage> getMissingLMSSetupAttribute() {
|
||||
return this.missingLMSSetupAttribute;
|
||||
}
|
||||
|
||||
|
@ -79,11 +86,11 @@ public final class LmsSetupTestResult {
|
|||
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);
|
||||
}
|
||||
|
||||
public static final LmsSetupTestResult ofMissingAttributes(final String... attrs) {
|
||||
public static final LmsSetupTestResult ofMissingAttributes(final APIMessage... attrs) {
|
||||
if (attrs == 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.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
|
||||
* 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 */
|
||||
public final class Result<T> {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(Result.class);
|
||||
|
||||
/** The resulting value. May be null if an error occurred */
|
||||
private final T value;
|
||||
/** 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
|
||||
public int hashCode() {
|
||||
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())
|
||||
|
||||
.createAction(ActionDefinition.INSTITUTION_SAVE)
|
||||
.withExec(formHandle::postChanges)
|
||||
.withExec(formHandle::processFormSave)
|
||||
.publishIf(() -> !isReadonly)
|
||||
|
||||
.createAction(ActionDefinition.INSTITUTION_CANCEL_MODIFY)
|
||||
|
|
|
@ -10,6 +10,7 @@ package ch.ethz.seb.sebserver.gui.content;
|
|||
|
||||
import java.util.function.BooleanSupplier;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.eclipse.swt.widgets.Composite;
|
||||
import org.slf4j.Logger;
|
||||
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.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.user.UserInfo;
|
||||
import ch.ethz.seb.sebserver.gbl.model.user.UserRole;
|
||||
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.form.FormBuilder;
|
||||
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.i18n.LocTextKey;
|
||||
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.TemplateComposer;
|
||||
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.NewLmsSetup;
|
||||
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.EntityGrantCheck;
|
||||
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory;
|
||||
|
@ -126,6 +131,9 @@ public class LmsSetupForm implements TemplateComposer {
|
|||
.putStaticValueIf(isNotNew,
|
||||
Domain.LMS_SETUP.ATTR_INSTITUTION_ID,
|
||||
String.valueOf(lmsSetup.getInstitutionId()))
|
||||
.putStaticValueIf(isNotNew,
|
||||
Domain.LMS_SETUP.ATTR_LMS_TYPE,
|
||||
String.valueOf(lmsSetup.getLmsType()))
|
||||
.addField(FormBuilder.singleSelection(
|
||||
Domain.LMS_SETUP.ATTR_INSTITUTION_ID,
|
||||
"sebserver.lmssetup.form.institution",
|
||||
|
@ -143,22 +151,21 @@ public class LmsSetupForm implements TemplateComposer {
|
|||
(lmsType != null) ? lmsType.name() : null,
|
||||
this.resourceService::lmsTypeResources)
|
||||
.readonlyIf(isNotNew))
|
||||
|
||||
.addField(FormBuilder.text(
|
||||
Domain.LMS_SETUP.ATTR_LMS_URL,
|
||||
"sebserver.lmssetup.form.url",
|
||||
lmsSetup.getLmsApiUrl())
|
||||
.withCondition(() -> isNotNew.getAsBoolean() && lmsType != LmsType.MOCKUP))
|
||||
.withCondition(() -> isNotNew.getAsBoolean()))
|
||||
.addField(FormBuilder.text(
|
||||
Domain.LMS_SETUP.ATTR_LMS_CLIENTNAME,
|
||||
"sebserver.lmssetup.form.clientname.lms",
|
||||
lmsSetup.getLmsAuthName())
|
||||
.withCondition(() -> isNotNew.getAsBoolean() && lmsType != LmsType.MOCKUP))
|
||||
.withCondition(() -> isNotNew.getAsBoolean()))
|
||||
.addField(FormBuilder.text(
|
||||
Domain.LMS_SETUP.ATTR_LMS_CLIENTSECRET,
|
||||
"sebserver.lmssetup.form.secret.lms")
|
||||
.asPasswordField()
|
||||
.withCondition(() -> isNotNew.getAsBoolean() && lmsType != LmsType.MOCKUP))
|
||||
.withCondition(() -> isNotNew.getAsBoolean()))
|
||||
|
||||
.buildFor((entityKey == null)
|
||||
? restService.getRestCall(NewLmsSetup.class)
|
||||
|
@ -176,6 +183,11 @@ public class LmsSetupForm implements TemplateComposer {
|
|||
.withEntityKey(entityKey)
|
||||
.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)
|
||||
.withEntityKey(entityKey)
|
||||
.withExec(restService::activation)
|
||||
|
@ -188,7 +200,7 @@ public class LmsSetupForm implements TemplateComposer {
|
|||
.publishIf(() -> writeGrant && readonly && istitutionActive && !lmsSetup.isActive())
|
||||
|
||||
.createAction(ActionDefinition.LMS_SETUP_SAVE)
|
||||
.withExec(formHandle::postChanges)
|
||||
.withExec(formHandle::processFormSave)
|
||||
.publishIf(() -> !readonly)
|
||||
|
||||
.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)
|
||||
.withExec(action -> {
|
||||
formHandle.postChanges(action);
|
||||
formHandle.processFormSave(action);
|
||||
if (ownAccount) {
|
||||
// NOTE: in this case the user changed the password of the own account
|
||||
// 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)
|
||||
.withExec(action -> {
|
||||
final Action postChanges = formHandle.postChanges(action);
|
||||
final Action postChanges = formHandle.processFormSave(action);
|
||||
if (ownAccount) {
|
||||
currentUser.refresh();
|
||||
pageContext.forwardToMainPage();
|
||||
|
|
|
@ -182,6 +182,11 @@ public enum ActionDefinition {
|
|||
ImageIcon.EDIT,
|
||||
LmsSetupForm.class,
|
||||
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(
|
||||
new LocTextKey("sebserver.overall.action.modify.cancel"),
|
||||
ImageIcon.CANCEL,
|
||||
|
@ -213,13 +218,13 @@ public enum ActionDefinition {
|
|||
public final Class<? extends RestCall<?>> restCallType;
|
||||
public final ActionDefinition activityAlias;
|
||||
public final String category;
|
||||
public final boolean readonly;
|
||||
public final Boolean readonly;
|
||||
|
||||
private ActionDefinition(
|
||||
final LocTextKey title,
|
||||
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(
|
||||
|
@ -227,7 +232,7 @@ public enum ActionDefinition {
|
|||
final Class<? extends TemplateComposer> contentPaneComposer,
|
||||
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(
|
||||
|
@ -236,7 +241,7 @@ public enum ActionDefinition {
|
|||
final Class<? extends TemplateComposer> contentPaneComposer,
|
||||
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(
|
||||
|
@ -246,7 +251,7 @@ public enum ActionDefinition {
|
|||
final Class<? extends RestCall<?>> restCallType,
|
||||
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(
|
||||
|
@ -267,7 +272,7 @@ public enum ActionDefinition {
|
|||
final Class<? extends RestCall<?>> restCallType,
|
||||
final ActionDefinition activityAlias,
|
||||
final String category,
|
||||
final boolean readonly) {
|
||||
final Boolean readonly) {
|
||||
|
||||
this.title = title;
|
||||
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(
|
||||
final Predicate<String> nameFilter,
|
||||
final Consumer<FormFieldAccessor> processor) {
|
||||
|
|
|
@ -13,9 +13,9 @@ import java.util.function.Consumer;
|
|||
import org.slf4j.Logger;
|
||||
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.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.service.i18n.I18nSupport;
|
||||
import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
|
||||
|
@ -50,12 +50,24 @@ public class FormHandle<T extends Entity> {
|
|||
this.i18nSupport = i18nSupport;
|
||||
}
|
||||
|
||||
public final Action postChanges(final Action action) {
|
||||
return doAPIPost(action.definition)
|
||||
.getOrThrow();
|
||||
/** Process an API post request to send and save the form field values
|
||||
* to the webservice and publishes a page event to return to read-only-view
|
||||
* 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(
|
||||
name -> true,
|
||||
fieldAccessor -> fieldAccessor.resetError());
|
||||
|
@ -63,34 +75,52 @@ public class FormHandle<T extends Entity> {
|
|||
return this.post
|
||||
.newBuilder()
|
||||
.withFormBinding(this.form)
|
||||
.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)
|
||||
;
|
||||
.call();
|
||||
}
|
||||
|
||||
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) {
|
||||
((RestCallError) error)
|
||||
.getErrorMessages()
|
||||
.stream()
|
||||
.filter(APIMessage.ErrorMessage.FIELD_VALIDATION::isOf)
|
||||
.map(FieldValidationError::new)
|
||||
.forEach(fve -> this.form.process(
|
||||
name -> name.equals(fve.fieldName),
|
||||
fieldAccessor -> showValidationError(fieldAccessor, fve)));
|
||||
return true;
|
||||
} else {
|
||||
log.error("Unexpected error while trying to post form: ", error);
|
||||
this.pageContext.notifyError(error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasAnyError() {
|
||||
return this.form.hasAnyError();
|
||||
}
|
||||
|
||||
private final void showValidationError(
|
||||
final FormFieldAccessor fieldAccessor,
|
||||
final FieldValidationError valError) {
|
||||
|
|
|
@ -206,6 +206,14 @@ public interface PageContext {
|
|||
* @param message the localized text key of the 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.
|
||||
*
|
||||
* @param pme the PageMessageException */
|
||||
|
|
|
@ -48,9 +48,12 @@ public final class Action implements Runnable {
|
|||
|
||||
this.definition = definition;
|
||||
this.originalPageContext = pageContext;
|
||||
final String readonly = pageContext.getAttribute(AttributeKeys.READ_ONLY, "true");
|
||||
this.pageContext = pageContext.withAttribute(
|
||||
AttributeKeys.READ_ONLY,
|
||||
String.valueOf(definition.readonly));
|
||||
definition.readonly != null
|
||||
? String.valueOf(definition.readonly)
|
||||
: readonly);
|
||||
}
|
||||
|
||||
@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) {
|
||||
this.exec = exec;
|
||||
return this;
|
||||
|
|
|
@ -313,7 +313,7 @@ public class PageContextImpl implements PageContext {
|
|||
final MessageBox messageBox = new Message(
|
||||
getShell(),
|
||||
this.i18nSupport.getText("sebserver.error.unexpected"),
|
||||
error.toString(),
|
||||
Utils.formatHTMLLines(errorMessage),
|
||||
SWT.ERROR);
|
||||
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"),
|
||||
MINIMIZE("minimize.png"),
|
||||
EDIT("edit.png"),
|
||||
TEST("test.png"),
|
||||
CANCEL("cancel.png"),
|
||||
CANCEL_EDIT("cancelEdit.png"),
|
||||
SHOW("show.png"),
|
||||
|
|
|
@ -23,16 +23,29 @@ import org.springframework.jdbc.datasource.DataSourceTransactionManager;
|
|||
|
||||
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
|
||||
@MapperScan(basePackages = "ch.ethz.seb.sebserver.webservice.datalayer.batis")
|
||||
@WebServiceProfile
|
||||
@Import(DataSourceAutoConfiguration.class)
|
||||
public class BatisConfig {
|
||||
|
||||
/** Name of the transaction manager bean for MyBatis based Spring controlled transactions */
|
||||
public static final String TRANSACTION_MANAGER = "transactionManager";
|
||||
/** Name of the sql session template bean of MyBatis */
|
||||
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";
|
||||
|
||||
/** Transaction manager bean for MyBatis based Spring controlled transactions */
|
||||
@Lazy
|
||||
@Bean(name = SQL_SESSION_FACTORY)
|
||||
public SqlSessionFactory sqlSessionFactory(final DataSource dataSource) throws Exception {
|
||||
|
@ -41,6 +54,7 @@ public class BatisConfig {
|
|||
return factoryBean.getObject();
|
||||
}
|
||||
|
||||
/** SQL session template bean of MyBatis */
|
||||
@Lazy
|
||||
@Bean(name = SQL_SESSION_TEMPLATE)
|
||||
public SqlSessionTemplate sqlSessionTemplate(final DataSource dataSource) throws Exception {
|
||||
|
@ -48,6 +62,7 @@ public class BatisConfig {
|
|||
return sqlSessionTemplate;
|
||||
}
|
||||
|
||||
/** SQL session factory bean of MyBatis */
|
||||
@Lazy
|
||||
@Bean(name = TRANSACTION_MANAGER)
|
||||
public DataSourceTransactionManager transactionManager(final DataSource dataSource) {
|
||||
|
|
|
@ -229,6 +229,16 @@ public interface AuthorizationService {
|
|||
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) {
|
||||
final EnumSet<UserRole> userRolesOfUserAccount = userAccount.getUserRoles();
|
||||
final SEBServerUser currentUser = getUserService().getCurrentUser();
|
||||
|
|
|
@ -50,6 +50,10 @@ public interface UserService {
|
|||
* @return an overall super user with all rights */
|
||||
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);
|
||||
|
||||
}
|
||||
|
|
|
@ -16,22 +16,30 @@ import java.util.LinkedHashSet;
|
|||
import java.util.Set;
|
||||
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.EntityType;
|
||||
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
|
||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||
import ch.ethz.seb.sebserver.gbl.util.Utils;
|
||||
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 {
|
||||
|
||||
/** Defines the type of the BulkAction */
|
||||
public final BulkActionType type;
|
||||
/** Defines the EntityType of the source entities of the BulkAction */
|
||||
public final EntityType sourceType;
|
||||
/** A Set of EntityKey defining all source-entities of the BulkAction */
|
||||
public final Set<EntityKey> sources;
|
||||
|
||||
/** A Set of EntityKey containing collected depending entities during dependency collection and processing phase */
|
||||
final Set<EntityKey> dependencies;
|
||||
/** A Set of EntityKey containing collected bulk action processing results during processing phase */
|
||||
final Set<Result<EntityKey>> result;
|
||||
|
||||
/** Indicates if this BulkAction has already been processed and is not valid anymore */
|
||||
boolean alreadyProcessed = false;
|
||||
|
||||
public BulkAction(
|
||||
|
|
|
@ -8,176 +8,15 @@
|
|||
|
||||
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 BulkActionService {
|
||||
public interface BulkActionService {
|
||||
|
||||
private final Map<EntityType, BulkActionSupportDAO<?>> supporter;
|
||||
private final UserActivityLogDAO userActivityLogDAO;
|
||||
void collectDependencies(BulkAction action);
|
||||
|
||||
public BulkActionService(
|
||||
final Collection<BulkActionSupportDAO<?>> supporter,
|
||||
final UserActivityLogDAO userActivityLogDAO) {
|
||||
Result<BulkAction> doBulkAction(BulkAction action);
|
||||
|
||||
this.supporter = new HashMap<>();
|
||||
for (final BulkActionSupportDAO<?> support : supporter) {
|
||||
this.supporter.put(support.entityType(), support);
|
||||
}
|
||||
this.userActivityLogDAO = userActivityLogDAO;
|
||||
}
|
||||
Result<EntityProcessingReport> createReport(BulkAction action);
|
||||
|
||||
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.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> {
|
||||
|
||||
/** 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 */
|
||||
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);
|
||||
|
||||
/** 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
|
||||
default Collection<Result<EntityKey>> processBulkAction(final BulkAction bulkAction) {
|
||||
final Set<EntityKey> all = bulkAction.extractKeys(entityType());
|
||||
|
|
|
@ -8,178 +8,30 @@
|
|||
|
||||
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 ClientCredentialService {
|
||||
public interface ClientCredentialService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ClientCredentialService.class);
|
||||
Result<ClientCredentials> createGeneratedClientCredentials();
|
||||
|
||||
static final String SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY = "sebserver.webservice.internalSecret";
|
||||
static final CharSequence DEFAULT_SALT = "b7dbe99bbfa3e21e";
|
||||
Result<ClientCredentials> createGeneratedClientCredentials(CharSequence salt);
|
||||
|
||||
private final Environment environment;
|
||||
ClientCredentials encryptedClientCredentials(ClientCredentials clientCredentials);
|
||||
|
||||
protected ClientCredentialService(final Environment environment) {
|
||||
this.environment = environment;
|
||||
}
|
||||
ClientCredentials encryptedClientCredentials(
|
||||
ClientCredentials clientCredentials,
|
||||
CharSequence salt);
|
||||
|
||||
public Result<ClientCredentials> createGeneratedClientCredentials() {
|
||||
return createGeneratedClientCredentials(null);
|
||||
}
|
||||
CharSequence getPlainClientId(ClientCredentials credentials);
|
||||
|
||||
public Result<ClientCredentials> createGeneratedClientCredentials(final CharSequence salt) {
|
||||
return Result.tryCatch(() -> {
|
||||
CharSequence getPlainClientId(ClientCredentials credentials, CharSequence salt);
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
CharSequence getPlainClientSecret(ClientCredentials credentials);
|
||||
|
||||
public ClientCredentials encryptedClientCredentials(final ClientCredentials clientCredentials) {
|
||||
return encryptedClientCredentials(clientCredentials, null);
|
||||
}
|
||||
CharSequence getPlainClientSecret(ClientCredentials credentials, CharSequence salt);
|
||||
|
||||
public ClientCredentials encryptedClientCredentials(
|
||||
final ClientCredentials clientCredentials,
|
||||
final CharSequence salt) {
|
||||
CharSequence getPlainAccessToken(ClientCredentials credentials);
|
||||
|
||||
final CharSequence secret = this.environment
|
||||
.getRequiredProperty(SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY);
|
||||
CharSequence getPlainAccessToken(ClientCredentials credentials, CharSequence salt);
|
||||
|
||||
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;
|
||||
|
||||
/** Adds some static logging support for DAO's */
|
||||
public final class DAOLoggingSupport {
|
||||
|
||||
public static final Logger log = LoggerFactory.getLogger(DAOLoggingSupport.class);
|
||||
|
|
|
@ -13,8 +13,10 @@ import org.springframework.util.LinkedMultiValueMap;
|
|||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
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.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.SebClientConfig;
|
||||
import ch.ethz.seb.sebserver.gbl.model.user.UserInfo;
|
||||
|
@ -39,7 +41,7 @@ public class FilterMap extends POSTMapper {
|
|||
}
|
||||
|
||||
public String getName() {
|
||||
return getSQLWildcard(UserInfo.FILTER_ATTR_NAME);
|
||||
return getSQLWildcard(Entity.FILTER_ATTR_NAME);
|
||||
}
|
||||
|
||||
public String getUserUsername() {
|
||||
|
@ -54,14 +56,14 @@ public class FilterMap extends POSTMapper {
|
|||
return getString(UserInfo.FILTER_ATTR_LANGUAGE);
|
||||
}
|
||||
|
||||
public String getLmsSetupName() {
|
||||
return getSQLWildcard(LmsSetup.FILTER_ATTR_NAME);
|
||||
}
|
||||
|
||||
public String getLmsSetupType() {
|
||||
return getString(LmsSetup.FILTER_ATTR_LMS_TYPE);
|
||||
}
|
||||
|
||||
public DateTime getQuizFromTime() {
|
||||
return JodaTimeTypeResolver.getDateTime(getString(QuizData.FILTER_ATTR_START_TIME));
|
||||
}
|
||||
|
||||
public DateTime getExamFromTime() {
|
||||
return JodaTimeTypeResolver.getDateTime(getString(Exam.FILTER_ATTR_FROM));
|
||||
}
|
||||
|
@ -74,8 +76,8 @@ public class FilterMap extends POSTMapper {
|
|||
return getString(Exam.FILTER_ATTR_QUIZ_ID);
|
||||
}
|
||||
|
||||
public Long getExamLmsSetupId() {
|
||||
return getLong(Exam.FILTER_ATTR_LMS_SETUP);
|
||||
public Long getLmsSetupId() {
|
||||
return getLong(LmsSetup.FILTER_ATTR_LMS_SETUP);
|
||||
}
|
||||
|
||||
public String getExamStatus() {
|
||||
|
|
|
@ -134,7 +134,7 @@ public class ExamDAOImpl implements ExamDAO {
|
|||
isEqualToWhenPresent(filterMap.getExamQuizId()))
|
||||
.and(
|
||||
ExamRecordDynamicSqlSupport.lmsSetupId,
|
||||
isEqualToWhenPresent(filterMap.getExamLmsSetupId()))
|
||||
isEqualToWhenPresent(filterMap.getLmsSetupId()))
|
||||
.and(
|
||||
ExamRecordDynamicSqlSupport.status,
|
||||
isEqualToWhenPresent(filterMap.getExamStatus()))
|
||||
|
@ -366,7 +366,7 @@ public class ExamDAOImpl implements ExamDAO {
|
|||
(map1, map2) -> Utils.mapPutAll(map1, map2));
|
||||
|
||||
return this.lmsAPIService
|
||||
.createLmsAPITemplate(lmsSetupId)
|
||||
.getLmsAPITemplate(lmsSetupId)
|
||||
.map(template -> template.getQuizzes(recordMapping.keySet()))
|
||||
.getOrThrow()
|
||||
.stream()
|
||||
|
|
|
@ -110,7 +110,7 @@ public class LmsSetupDAOImpl implements LmsSetupDAO {
|
|||
isEqualToWhenPresent(filterMap.getInstitutionId()))
|
||||
.and(
|
||||
LmsSetupRecordDynamicSqlSupport.name,
|
||||
isLikeWhenPresent(filterMap.getLmsSetupName()))
|
||||
isLikeWhenPresent(filterMap.getName()))
|
||||
.and(
|
||||
LmsSetupRecordDynamicSqlSupport.lmsType,
|
||||
isEqualToWhenPresent(filterMap.getLmsSetupType()))
|
||||
|
|
|
@ -8,13 +8,84 @@
|
|||
|
||||
package ch.ethz.seb.sebserver.webservice.servicelayer.lms;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
|
||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||
import java.util.List;
|
||||
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 {
|
||||
|
||||
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;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
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.institution.LmsSetup;
|
||||
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
|
||||
import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails;
|
||||
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 {
|
||||
|
||||
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();
|
||||
|
||||
Result<Page<QuizData>> getQuizzes(
|
||||
String name,
|
||||
Long from,
|
||||
String sort,
|
||||
int pageNumber,
|
||||
int pageSize);
|
||||
/** Get a Result of an unsorted List of filtered QuizData from the LMS course/quiz API
|
||||
*
|
||||
* @param filterMap the FilterMap to get a filtered result. For possible filter attributes
|
||||
* see documentation on QuizData
|
||||
* @return Result of an unsorted List of filtered QuizData from the LMS course/quiz API
|
||||
* or refer to an error when happened */
|
||||
Result<List<QuizData>> getQuizzes(FilterMap filterMap);
|
||||
|
||||
Collection<Result<QuizData>> getQuizzes(Set<String> ids);
|
||||
|
||||
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;
|
||||
|
||||
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.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
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.stereotype.Service;
|
||||
|
||||
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.profile.WebServiceProfile;
|
||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentialService;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.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.lms.LmsAPIService;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
|
||||
import ch.ethz.seb.sebserver.webservice.weblayer.api.IllegalAPIArgumentException;
|
||||
|
||||
@Lazy
|
||||
@Service
|
||||
@WebServiceProfile
|
||||
public class LmsAPIServiceImpl implements LmsAPIService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(LmsAPIServiceImpl.class);
|
||||
|
||||
private final LmsSetupDAO lmsSetupDAO;
|
||||
private final ClientCredentialService internalEncryptionService;
|
||||
private final ClientCredentialService clientCredentialService;
|
||||
private final ClientHttpRequestFactory clientHttpRequestFactory;
|
||||
private final String[] openEdxAlternativeTokenRequestPaths;
|
||||
|
||||
// TODO internal caching of LmsAPITemplate per LmsSetup (Id)
|
||||
private final Map<CacheKey, LmsAPITemplate> cache = new ConcurrentHashMap<>();
|
||||
|
||||
public LmsAPIServiceImpl(
|
||||
final LmsSetupDAO lmsSetupDAO,
|
||||
final ClientCredentialService internalEncryptionService,
|
||||
final ClientCredentialService clientCredentialService,
|
||||
final ClientHttpRequestFactory clientHttpRequestFactory,
|
||||
@Value("${sebserver.lms.openedix.api.token.request.paths}") final String alternativeTokenRequestPaths) {
|
||||
|
||||
this.lmsSetupDAO = lmsSetupDAO;
|
||||
this.internalEncryptionService = internalEncryptionService;
|
||||
this.clientCredentialService = clientCredentialService;
|
||||
this.clientHttpRequestFactory = clientHttpRequestFactory;
|
||||
|
||||
this.openEdxAlternativeTokenRequestPaths = (alternativeTokenRequestPaths != null)
|
||||
|
@ -48,60 +66,167 @@ public class LmsAPIServiceImpl implements LmsAPIService {
|
|||
: null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<LmsAPITemplate> createLmsAPITemplate(final Long lmsSetupId) {
|
||||
return this.lmsSetupDAO
|
||||
.byPK(lmsSetupId)
|
||||
.flatMap(this::createLmsAPITemplate);
|
||||
}
|
||||
|
||||
@Override
|
||||
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));
|
||||
/** Listen to LmsSetupChangeEvent to release an affected LmsAPITemplate from cache
|
||||
*
|
||||
* @param event the event holding the changed LmsSetup */
|
||||
@EventListener
|
||||
public void notifyLmsSetupChange(final LmsSetupChangeEvent event) {
|
||||
final LmsSetup lmsSetup = event.getLmsSetup();
|
||||
if (lmsSetup == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug("LmsSetup changed. Update cache by removeing eventually used references");
|
||||
|
||||
this.cache.remove(new CacheKey(lmsSetup.getModelId(), 0));
|
||||
}
|
||||
|
||||
// @Override
|
||||
// public Result<InputStream> createSEBStartConfiguration(final Long lmsSetupId) {
|
||||
// return this.lmsSetupDAO
|
||||
// .byPK(lmsSetupId)
|
||||
// .flatMap(this::createSEBStartConfiguration);
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public Result<InputStream> createSEBStartConfiguration(final LmsSetup lmsSetup) {
|
||||
//
|
||||
// // 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
|
||||
// //
|
||||
// // To Clarify : The format of a SEB start configuration
|
||||
// // 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
|
||||
//
|
||||
// return Result.tryCatch(() -> {
|
||||
// try {
|
||||
// return new ByteArrayInputStream("TODO".getBytes("UTF-8"));
|
||||
// } catch (final UnsupportedEncodingException e) {
|
||||
// throw new RuntimeException("cause: ", e);
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
@Override
|
||||
public Result<Page<QuizData>> requestQuizDataPage(
|
||||
final int pageNumber,
|
||||
final int pageSize,
|
||||
final String sort,
|
||||
final FilterMap filterMap) {
|
||||
|
||||
return getAllQuizzesFromLMSSetups(filterMap)
|
||||
.map(LmsAPIService.quizzesSortFunction(sort))
|
||||
.map(LmsAPIService.quizzesToPageFunction(sort, pageNumber, pageSize));
|
||||
}
|
||||
|
||||
/** Collect all QuizData from all affecting LmsSetup.
|
||||
* If filterMap contains a LmsSetup identifier, only the QuizData from that LmsSetup is collected.
|
||||
* Otherwise QuizData from all active LmsSetup of the current institution are collected.
|
||||
*
|
||||
* @param filterMap the FilterMap containing either an LmsSetup identifier or an institution identifier
|
||||
* @return list of QuizData from all affecting LmsSetup */
|
||||
private Result<List<QuizData>> getAllQuizzesFromLMSSetups(final FilterMap filterMap) {
|
||||
|
||||
return Result.tryCatch(() -> {
|
||||
final Long lmsSetupId = filterMap.getLmsSetupId();
|
||||
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.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.model.Domain.LMS_SETUP;
|
||||
import ch.ethz.seb.sebserver.gbl.model.Page;
|
||||
import org.slf4j.Logger;
|
||||
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.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.user.ExamineeAccountDetails;
|
||||
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.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;
|
||||
|
||||
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_SECRET = "mockupLmsClientSecret";
|
||||
|
||||
private final ClientCredentialService clientCredentialService;
|
||||
private final LmsSetupDAO lmsSetupDao;
|
||||
private final LmsSetup setup;
|
||||
|
||||
private ClientCredentials credentials = null;
|
||||
private final LmsSetup lmsSetup;
|
||||
private final ClientCredentials credentials;
|
||||
private final Collection<QuizData> mockups;
|
||||
|
||||
MockupLmsAPITemplate(
|
||||
final LmsSetupDAO lmsSetupDao,
|
||||
final LmsSetup setup,
|
||||
final LmsSetup lmsSetup,
|
||||
final ClientCredentials credentials,
|
||||
final ClientCredentialService clientCredentialService) {
|
||||
|
||||
this.lmsSetupDao = lmsSetupDao;
|
||||
this.lmsSetup = lmsSetup;
|
||||
this.clientCredentialService = clientCredentialService;
|
||||
if (!setup.isActive() || setup.lmsType != LmsType.MOCKUP) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
this.credentials = credentials;
|
||||
|
||||
this.setup = setup;
|
||||
this.mockups = new ArrayList<>();
|
||||
this.mockups.add(new QuizData(
|
||||
"quiz1", "Demo Quiz 1", "Demo Quit Mockup",
|
||||
|
@ -79,88 +75,45 @@ final class MockupLmsAPITemplate implements LmsAPITemplate {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Result<LmsSetup> lmsSetup() {
|
||||
return Result.of(this.setup);
|
||||
public LmsSetup lmsSetup() {
|
||||
return this.lmsSetup;
|
||||
}
|
||||
|
||||
@Override
|
||||
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();
|
||||
} else {
|
||||
return LmsSetupTestResult.ofMissingAttributes(
|
||||
LMS_SETUP.ATTR_LMS_URL,
|
||||
LMS_SETUP.ATTR_LMS_CLIENTNAME,
|
||||
LMS_SETUP.ATTR_LMS_CLIENTSECRET);
|
||||
return LmsSetupTestResult.ofTokenRequestError("Illegal access");
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
public Result<Page<QuizData>> getQuizzes(
|
||||
final String name,
|
||||
final Long from,
|
||||
final String sort,
|
||||
final int pageNumber,
|
||||
final int pageSize) {
|
||||
public Result<List<QuizData>> getQuizzes(final FilterMap filterMap) {
|
||||
|
||||
return Result.tryCatch(() -> {
|
||||
initCredentials();
|
||||
authenticate();
|
||||
if (this.credentials == null) {
|
||||
throw new IllegalArgumentException("Wrong clientId or secret");
|
||||
}
|
||||
|
||||
final int startIndex = pageNumber * pageSize;
|
||||
final int endIndex = startIndex + pageSize;
|
||||
int index = 0;
|
||||
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);
|
||||
return this.mockups.stream()
|
||||
.filter(LmsAPIService.quizzeFilterFunction(filterMap))
|
||||
.collect(Collectors.toList());
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Result<QuizData>> getQuizzes(final Set<String> ids) {
|
||||
initCredentials();
|
||||
authenticate();
|
||||
if (this.credentials == null) {
|
||||
throw new IllegalArgumentException("Wrong clientId or secret");
|
||||
}
|
||||
|
@ -173,7 +126,7 @@ final class MockupLmsAPITemplate implements LmsAPITemplate {
|
|||
|
||||
@Override
|
||||
public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeUserId) {
|
||||
initCredentials();
|
||||
authenticate();
|
||||
if (this.credentials == null) {
|
||||
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"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
this.credentials = null;
|
||||
}
|
||||
|
||||
private void initCredentials() {
|
||||
private boolean authenticate() {
|
||||
try {
|
||||
this.credentials = this.lmsSetupDao
|
||||
.getLmsAPIAccessCredentials(this.setup.getModelId())
|
||||
.getOrThrow();
|
||||
|
||||
final CharSequence plainClientId = this.clientCredentialService.getPlainClientId(this.credentials);
|
||||
if (!"lmsMockupClientId".equals(plainClientId)) {
|
||||
throw new IllegalAccessError();
|
||||
throw new IllegalAccessException("Wrong client credential");
|
||||
}
|
||||
|
||||
final CharSequence plainClientSecret = this.clientCredentialService.getPlainClientSecret(this.credentials);
|
||||
if (!"lmsMockupSecret".equals(plainClientSecret)) {
|
||||
throw new IllegalAccessError();
|
||||
throw new IllegalAccessException("Wrong client credential");
|
||||
}
|
||||
|
||||
return true;
|
||||
} 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.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
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.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.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.model.Page;
|
||||
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
|
||||
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.LmsType;
|
||||
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
|
||||
import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails;
|
||||
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.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;
|
||||
|
||||
/** Implements the LmsAPITemplate for Open edX LMS Course API access.
|
||||
*
|
||||
* See also: https://course-catalog-api-guide.readthedocs.io */
|
||||
final class OpenEdxLmsAPITemplate implements LmsAPITemplate {
|
||||
|
||||
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_START_URL_PREFIX = "/courses/";
|
||||
|
||||
private final String lmsSetupId;
|
||||
private final LmsSetupDAO lmsSetupDAO;
|
||||
private final LmsSetup lmsSetup;
|
||||
private final ClientCredentials credentials;
|
||||
private final ClientHttpRequestFactory clientHttpRequestFactory;
|
||||
private final ClientCredentialService clientCredentialService;
|
||||
private final Set<String> knownTokenAccessPaths;
|
||||
|
||||
private OAuth2RestTemplate restTemplate = null;
|
||||
private SupplierWithCircuitBreaker<List<QuizData>> allQuizzesSupplier = null;
|
||||
|
||||
OpenEdxLmsAPITemplate(
|
||||
final String lmsSetupId,
|
||||
final LmsSetupDAO lmsSetupDAO,
|
||||
final LmsSetup lmsSetup,
|
||||
final ClientCredentials credentials,
|
||||
final ClientCredentialService clientCredentialService,
|
||||
final ClientHttpRequestFactory clientHttpRequestFactory,
|
||||
final String[] alternativeTokenRequestPaths) {
|
||||
|
||||
this.lmsSetupId = lmsSetupId;
|
||||
this.lmsSetupDAO = lmsSetupDAO;
|
||||
this.clientHttpRequestFactory = clientHttpRequestFactory;
|
||||
this.lmsSetup = lmsSetup;
|
||||
this.clientCredentialService = clientCredentialService;
|
||||
|
||||
this.credentials = credentials;
|
||||
this.clientHttpRequestFactory = clientHttpRequestFactory;
|
||||
this.knownTokenAccessPaths = new HashSet<>();
|
||||
this.knownTokenAccessPaths.add(OPEN_EDX_DEFAULT_TOKEN_REQUEST_PATH);
|
||||
if (alternativeTokenRequestPaths != null) {
|
||||
|
@ -77,87 +89,45 @@ final class OpenEdxLmsAPITemplate implements LmsAPITemplate {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Result<LmsSetup> lmsSetup() {
|
||||
return this.lmsSetupDAO
|
||||
.byModelId(this.lmsSetupId);
|
||||
public LmsSetup lmsSetup() {
|
||||
return this.lmsSetup;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LmsSetupTestResult testLmsSetup() {
|
||||
|
||||
final LmsSetup lmsSetup = lmsSetup().getOrThrow();
|
||||
|
||||
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);
|
||||
}
|
||||
log.info("Test Lms Binding for OpenEdX and LmsSetup: {}", this.lmsSetup);
|
||||
|
||||
final List<APIMessage> missingAttrs = attributeValidation(this.credentials);
|
||||
if (!missingAttrs.isEmpty()) {
|
||||
return LmsSetupTestResult.ofMissingAttributes(missingAttrs);
|
||||
}
|
||||
|
||||
// request OAuth2 access token on OpenEdx API
|
||||
initRestTemplateAndRequestAccessToken(lmsSetup);
|
||||
initRestTemplateAndRequestAccessToken();
|
||||
if (this.restTemplate == null) {
|
||||
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);
|
||||
}
|
||||
|
||||
// query quizzes TODO!?
|
||||
|
||||
return LmsSetupTestResult.ofOkay();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Page<QuizData>> getQuizzes(
|
||||
final String name,
|
||||
final Long from,
|
||||
final String sort,
|
||||
final int pageNumber,
|
||||
final int pageSize) {
|
||||
public Result<List<QuizData>> getQuizzes(final FilterMap filterMap) {
|
||||
return this.initRestTemplateAndRequestAccessToken()
|
||||
.flatMap(this::getAllQuizes)
|
||||
.map(LmsAPIService.quizzesFilterFunction(filterMap));
|
||||
}
|
||||
|
||||
return this.lmsSetup()
|
||||
.flatMap(this::initRestTemplateAndRequestAccessToken)
|
||||
.map(lmsSetup -> {
|
||||
|
||||
// TODO sort and pagination
|
||||
final HttpHeaders httpHeaders = new HttpHeaders();
|
||||
|
||||
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);
|
||||
});
|
||||
public ResponseEntity<EdXPage> getEdxPage(final String pageURI) {
|
||||
final HttpHeaders httpHeaders = new HttpHeaders();
|
||||
return this.restTemplate.exchange(
|
||||
pageURI,
|
||||
HttpMethod.GET,
|
||||
new HttpEntity<>(httpHeaders),
|
||||
EdXPage.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -172,20 +142,15 @@ final class OpenEdxLmsAPITemplate implements LmsAPITemplate {
|
|||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
this.restTemplate = null;
|
||||
}
|
||||
private Result<LmsSetup> initRestTemplateAndRequestAccessToken() {
|
||||
|
||||
private Result<LmsSetup> initRestTemplateAndRequestAccessToken(final LmsSetup lmsSetup) {
|
||||
|
||||
log.info("Initialize Rest Template for OpenEdX API access. LmsSetup: {}", lmsSetup);
|
||||
log.info("Initialize Rest Template for OpenEdX API access. LmsSetup: {}", this.lmsSetup);
|
||||
|
||||
return Result.tryCatch(() -> {
|
||||
if (this.restTemplate != null) {
|
||||
try {
|
||||
this.restTemplate.getAccessToken();
|
||||
return lmsSetup;
|
||||
return this.lmsSetup;
|
||||
} catch (final Exception e) {
|
||||
log.warn(
|
||||
"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();
|
||||
while (tokenAccessPaths.hasNext()) {
|
||||
final String accessTokenRequestPath = tokenAccessPaths.next();
|
||||
try {
|
||||
|
||||
final OAuth2RestTemplate template = createRestTemplate(
|
||||
lmsSetup,
|
||||
credentials,
|
||||
this.lmsSetup,
|
||||
this.credentials,
|
||||
accessTokenRequestPath);
|
||||
|
||||
final OAuth2AccessToken accessToken = template.getAccessToken();
|
||||
if (accessToken != null) {
|
||||
this.restTemplate = template;
|
||||
return lmsSetup;
|
||||
return this.lmsSetup;
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
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.setClientId(plainClientId.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);
|
||||
template.setRequestFactory(this.clientHttpRequestFactory);
|
||||
template.setAccessTokenProvider(new EdxClientCredentialsAccessTokenProvider());
|
||||
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(
|
||||
final LmsSetup lmsSetup,
|
||||
final CourseData courseData) {
|
||||
|
@ -260,14 +254,16 @@ final class OpenEdxLmsAPITemplate implements LmsAPITemplate {
|
|||
startURI);
|
||||
}
|
||||
|
||||
/** Maps a OpenEdX course API course page */
|
||||
static final class EdXPage {
|
||||
public Integer count;
|
||||
public Integer previous;
|
||||
public String previous;
|
||||
public Integer num_pages;
|
||||
public Integer next;
|
||||
public String next;
|
||||
public List<CourseData> results;
|
||||
}
|
||||
|
||||
/** Maps the OpenEdX course API course data */
|
||||
static final class CourseData {
|
||||
public String id;
|
||||
public String course_id;
|
||||
|
@ -278,41 +274,31 @@ final class OpenEdxLmsAPITemplate implements LmsAPITemplate {
|
|||
public String end;
|
||||
}
|
||||
|
||||
/*
|
||||
* pagination
|
||||
* count 2
|
||||
* previous null
|
||||
* num_pages 1
|
||||
* next null
|
||||
* results
|
||||
* 0
|
||||
* blocks_url "http://ralph.ethz.ch:18000/api/courses/v1/blocks/?course_id=course-v1%3AedX%2BDemoX%2BDemo_Course"
|
||||
* effort null
|
||||
* end null
|
||||
* enrollment_start null
|
||||
* enrollment_end null
|
||||
* id "course-v1:edX+DemoX+Demo_Course"
|
||||
* media
|
||||
* course_image
|
||||
* uri "/asset-v1:edX+DemoX+Demo_Course+type@asset+block@images_course_image.jpg"
|
||||
* course_video
|
||||
* uri null
|
||||
* image
|
||||
* raw "http://ralph.ethz.ch:18000/asset-v1:edX+DemoX+Demo_Course+type@asset+block@images_course_image.jpg"
|
||||
* small "http://ralph.ethz.ch:18000/asset-v1:edX+DemoX+Demo_Course+type@asset+block@images_course_image.jpg"
|
||||
* large "http://ralph.ethz.ch:18000/asset-v1:edX+DemoX+Demo_Course+type@asset+block@images_course_image.jpg"
|
||||
* name "edX Demonstration Course"
|
||||
* 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"
|
||||
*/
|
||||
/** A custom ClientCredentialsAccessTokenProvider that adapts the access token request to Open edX
|
||||
* access token request protocol using a form-URL-encoded POST request according to:
|
||||
* https://course-catalog-api-guide.readthedocs.io/en/latest/authentication/index.html#getting-an-access-token */
|
||||
private class EdxClientCredentialsAccessTokenProvider extends ClientCredentialsAccessTokenProvider {
|
||||
|
||||
@Override
|
||||
public OAuth2AccessToken obtainAccessToken(
|
||||
final OAuth2ProtectedResourceDetails details,
|
||||
final AccessTokenRequest request)
|
||||
throws UserRedirectRequiredException,
|
||||
AccessDeniedException,
|
||||
OAuth2AccessDeniedException {
|
||||
|
||||
final ClientCredentialsResourceDetails resource = (ClientCredentialsResourceDetails) details;
|
||||
final HttpHeaders headers = new HttpHeaders();
|
||||
headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE);
|
||||
|
||||
final MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
|
||||
params.add("grant_type", "client_credentials");
|
||||
params.add("token_type", "jwt");
|
||||
params.add("client_id", resource.getClientId());
|
||||
params.add("client_secret", resource.getClientSecret());
|
||||
|
||||
return retrieveToken(request, resource, params, headers);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -137,7 +137,7 @@ public class APIExceptionHandler extends ResponseEntityExceptionHandler {
|
|||
final WebRequest request) {
|
||||
|
||||
return new ResponseEntity<>(
|
||||
Arrays.asList(ex.getAPIMessage()),
|
||||
ex.getAPIMessages(),
|
||||
HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
|
|
|
@ -253,7 +253,7 @@ public class ExamAdministrationController extends ActivatableEntityController<Ex
|
|||
final String quizId = postParams.getString(QuizData.QUIZ_ATTR_ID);
|
||||
|
||||
final LmsAPITemplate lmsAPITemplate = this.lmsAPIService
|
||||
.createLmsAPITemplate(lmsSetupId)
|
||||
.getLmsAPITemplate(lmsSetupId)
|
||||
.getOrThrow();
|
||||
|
||||
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 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.POSTMapper;
|
||||
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);
|
||||
|
||||
return this.lmsAPIService.createLmsAPITemplate(modelId)
|
||||
final LmsSetupTestResult result = this.lmsAPIService.getLmsAPITemplate(modelId)
|
||||
.map(template -> template.testLmsSetup())
|
||||
.getOrThrow();
|
||||
|
||||
if (result.missingLMSSetupAttribute != null && !result.missingLMSSetupAttribute.isEmpty()) {
|
||||
throw new APIMessageException(result.missingLMSSetupAttribute);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
package ch.ethz.seb.sebserver.webservice.weblayer.api;
|
||||
|
||||
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.RequestMethod;
|
||||
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.EntityType;
|
||||
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.Page;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
|
||||
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.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.LmsAPITemplate;
|
||||
|
||||
@WebServiceProfile
|
||||
@RestController
|
||||
|
@ -57,26 +56,20 @@ public class QuizImportController {
|
|||
name = Entity.FILTER_ATTR_INSTITUTION,
|
||||
required = true,
|
||||
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_SIZE, required = false) final Integer pageSize,
|
||||
@RequestParam(name = Page.ATTR_SORT, required = false) final String sort) {
|
||||
|
||||
final LmsAPITemplate lmsAPITemplate = this.lmsAPIService
|
||||
.createLmsAPITemplate(lmsSetupId)
|
||||
.getOrThrow();
|
||||
@RequestParam(name = Page.ATTR_SORT, required = false) final String sort,
|
||||
@RequestParam final MultiValueMap<String, String> allRequestParams) {
|
||||
|
||||
this.authorization.check(
|
||||
PrivilegeType.READ_ONLY,
|
||||
EntityType.EXAM,
|
||||
institutionId);
|
||||
|
||||
return lmsAPITemplate.getQuizzes(
|
||||
nameLike,
|
||||
Utils.dateTimeStringToTimestamp(startTime, null),
|
||||
sort,
|
||||
final FilterMap filterMap = new FilterMap(allRequestParams);
|
||||
filterMap.putIfAbsent(Entity.FILTER_ATTR_INSTITUTION, String.valueOf(institutionId));
|
||||
|
||||
return this.lmsAPIService.requestQuizDataPage(
|
||||
(pageNumber != null)
|
||||
? pageNumber
|
||||
: 1,
|
||||
|
@ -84,7 +77,9 @@ public class QuizImportController {
|
|||
? (pageSize <= this.maxPageSize)
|
||||
? pageSize
|
||||
: this.maxPageSize
|
||||
: this.defaultPageSize)
|
||||
: this.defaultPageSize,
|
||||
sort,
|
||||
filterMap)
|
||||
.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.password.wrong=Old password is wrong
|
||||
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.page.message=Information
|
||||
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.modify=Edit Selected
|
||||
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.activate=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_SQL_MODE=@@SQL_MODE, SQL_MODE='TRADITIONAL,ALLOW_INVALID_DATES';
|
||||
|
||||
|
||||
-- -----------------------------------------------------
|
||||
-- Schema SEBServer
|
||||
-- -----------------------------------------------------
|
||||
|
||||
|
||||
|
||||
-- -----------------------------------------------------
|
||||
-- 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
|
||||
public void testEncryptDecryptClientCredentials() {
|
||||
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");
|
||||
|
||||
final String clientName = "simpleClientName";
|
||||
|
||||
final ClientCredentialService service = new ClientCredentialService(envMock);
|
||||
final ClientCredentialServiceImpl service = new ClientCredentialServiceImpl(envMock);
|
||||
String encrypted =
|
||||
service.encrypt(clientName, "secret1", ClientCredentialService.DEFAULT_SALT).toString();
|
||||
String decrypted = service.decrypt(encrypted, "secret1", ClientCredentialService.DEFAULT_SALT).toString();
|
||||
service.encrypt(clientName, "secret1", ClientCredentialServiceImpl.DEFAULT_SALT).toString();
|
||||
String decrypted = service.decrypt(encrypted, "secret1", ClientCredentialServiceImpl.DEFAULT_SALT).toString();
|
||||
|
||||
assertEquals(clientName, decrypted);
|
||||
|
||||
final String clientSecret = "fbjreij39ru29305ruࣣàèLöäöäü65%(/%(ç87";
|
||||
|
||||
encrypted =
|
||||
service.encrypt(clientSecret, "secret1", ClientCredentialService.DEFAULT_SALT).toString();
|
||||
decrypted = service.decrypt(encrypted, "secret1", ClientCredentialService.DEFAULT_SALT).toString();
|
||||
service.encrypt(clientSecret, "secret1", ClientCredentialServiceImpl.DEFAULT_SALT).toString();
|
||||
decrypted = service.decrypt(encrypted, "secret1", ClientCredentialServiceImpl.DEFAULT_SALT).toString();
|
||||
|
||||
assertEquals(clientSecret, decrypted);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue