SEBSERV-29 implementation of quiz data search from and LmsAPITemplate

This commit is contained in:
anhefti 2019-03-14 13:32:26 +01:00
parent c51016a548
commit 7b2f7228af
49 changed files with 1371 additions and 714 deletions

View file

@ -14,6 +14,11 @@ import org.joda.time.format.DateTimeFormatter;
/** Global Constants used in SEB Server web-service as well as in web-gui component */ /** Global Constants used in SEB Server web-service as well as in web-gui component */
public final class Constants { public final class Constants {
public static final long SECOND_IN_MILLIS = 1000;
public static final long MINUTE_IN_MILLIS = 60 * SECOND_IN_MILLIS;
public static final long HOUR_IN_MILLIS = 60 * MINUTE_IN_MILLIS;
public static final long DAY_IN_MILLIS = 24 * HOUR_IN_MILLIS;
public static final Character LIST_SEPARATOR_CHAR = ','; public static final Character LIST_SEPARATOR_CHAR = ',';
public static final String LIST_SEPARATOR = ","; public static final String LIST_SEPARATOR = ",";
public static final String EMPTY_NOTE = "--"; public static final String EMPTY_NOTE = "--";

View file

@ -42,7 +42,9 @@ public final class API {
public static final String LMS_SETUP_ENDPOINT = "/lms_setup"; public static final String LMS_SETUP_ENDPOINT = "/lms_setup";
public static final String LMS_SETUP_TEST_PATH_SEGMENT = "/test"; public static final String LMS_SETUP_TEST_PATH_SEGMENT = "/test";
public static final String LMS_SETUP_TEST_ENDPOINT = LMS_SETUP_ENDPOINT + LMS_SETUP_TEST_PATH_SEGMENT; public static final String LMS_SETUP_TEST_ENDPOINT = LMS_SETUP_ENDPOINT
+ LMS_SETUP_TEST_PATH_SEGMENT
+ MODEL_ID_VAR_PATH_SEGMENT;
public static final String USER_ACCOUNT_ENDPOINT = "/useraccount"; public static final String USER_ACCOUNT_ENDPOINT = "/useraccount";

View file

@ -169,25 +169,30 @@ public class APIMessage implements Serializable {
private static final long serialVersionUID = 1453431210820677296L; private static final long serialVersionUID = 1453431210820677296L;
private final APIMessage apiMessage; private final Collection<APIMessage> apiMessages;
public APIMessageException(final Collection<APIMessage> apiMessages) {
super();
this.apiMessages = apiMessages;
}
public APIMessageException(final APIMessage apiMessage) { public APIMessageException(final APIMessage apiMessage) {
super(); super();
this.apiMessage = apiMessage; this.apiMessages = Arrays.asList(apiMessage);
} }
public APIMessageException(final ErrorMessage errorMessage) { public APIMessageException(final ErrorMessage errorMessage) {
super(); super();
this.apiMessage = errorMessage.of(); this.apiMessages = Arrays.asList(errorMessage.of());
} }
public APIMessageException(final ErrorMessage errorMessage, final String detail, final String... attributes) { public APIMessageException(final ErrorMessage errorMessage, final String detail, final String... attributes) {
super(); super();
this.apiMessage = errorMessage.of(detail, attributes); this.apiMessages = Arrays.asList(errorMessage.of(detail, attributes));
} }
public APIMessage getAPIMessage() { public Collection<APIMessage> getAPIMessages() {
return this.apiMessage; return this.apiMessages;
} }
} }

View file

@ -27,8 +27,6 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.GrantEntity;
public final class Exam implements GrantEntity, Activatable { public final class Exam implements GrantEntity, Activatable {
public static final String FILTER_ATTR_LMS_SETUP = "lms_setup";
public static final String FILTER_ATTR_NAME = "name_like";
public static final String FILTER_ATTR_STATUS = "status"; public static final String FILTER_ATTR_STATUS = "status";
public static final String FILTER_ATTR_TYPE = "type"; public static final String FILTER_ATTR_TYPE = "type";
public static final String FILTER_ATTR_FROM = "from"; public static final String FILTER_ATTR_FROM = "from";

View file

@ -8,6 +8,8 @@
package ch.ethz.seb.sebserver.gbl.model.exam; package ch.ethz.seb.sebserver.gbl.model.exam;
import java.util.Comparator;
import org.joda.time.DateTime; import org.joda.time.DateTime;
import org.joda.time.DateTimeZone; import org.joda.time.DateTimeZone;
import org.joda.time.LocalDateTime; import org.joda.time.LocalDateTime;
@ -16,10 +18,12 @@ import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import ch.ethz.seb.sebserver.gbl.Constants; import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.model.Entity;
import ch.ethz.seb.sebserver.webservice.servicelayer.PaginationService.SortOrder;
public final class QuizData { public final class QuizData implements Entity {
public static final String FILTER_ATTR_NAME = "name_like";
public static final String FILTER_ATTR_START_TIME = "start_timestamp"; public static final String FILTER_ATTR_START_TIME = "start_timestamp";
public static final String QUIZ_ATTR_ID = "quiz_id"; public static final String QUIZ_ATTR_ID = "quiz_id";
@ -84,10 +88,25 @@ public final class QuizData {
this.startURL = startURL; this.startURL = startURL;
} }
@Override
public String getModelId() {
if (this.id == null) {
return null;
}
return String.valueOf(this.id);
}
@Override
public EntityType entityType() {
return EntityType.EXAM;
}
public String geId() { public String geId() {
return this.id; return this.id;
} }
@Override
public String getName() { public String getName() {
return this.name; return this.name;
} }
@ -140,4 +159,62 @@ public final class QuizData {
+ ", endTime=" + this.endTime + ", startURL=" + this.startURL + "]"; + ", endTime=" + this.endTime + ", startURL=" + this.startURL + "]";
} }
public static Comparator<QuizData> getIdComparator(final boolean descending) {
return (qd1, qd2) -> ((qd1 == qd2)
? 0
: (qd1 == null || qd1.id == null)
? 1
: (qd2 == null || qd2.id == null)
? -1
: qd1.id.compareTo(qd2.id))
* ((descending) ? -1 : 1);
}
public static Comparator<QuizData> getNameComparator(final boolean descending) {
return (qd1, qd2) -> ((qd1 == qd2)
? 0
: (qd1 == null || qd1.name == null)
? 1
: (qd2 == null || qd2.name == null)
? -1
: qd1.name.compareTo(qd2.name))
* ((descending) ? -1 : 1);
}
public static Comparator<QuizData> getStartTimeComparator(final boolean descending) {
return (qd1, qd2) -> ((qd1 == qd2)
? 0
: (qd1 == null || qd1.startTime == null)
? 1
: (qd2 == null || qd2.startTime == null)
? -1
: qd1.startTime.compareTo(qd2.startTime))
* ((descending) ? -1 : 1);
}
public static Comparator<QuizData> getEndTimeComparator(final boolean descending) {
return (qd1, qd2) -> ((qd1 == qd2)
? 0
: (qd1 == null || qd1.endTime == null)
? 1
: (qd2 == null || qd2.endTime == null)
? -1
: qd1.endTime.compareTo(qd2.endTime))
* ((descending) ? -1 : 1);
}
public static Comparator<QuizData> getComparator(final String sort) {
final boolean descending = SortOrder.getSortOrder(sort) == SortOrder.DESCENDING;
final String sortParam = SortOrder.decode(sort);
if (QUIZ_ATTR_NAME.equals(sortParam)) {
return getNameComparator(descending);
} else if (QUIZ_ATTR_START_TIME.equals(sortParam)) {
return getStartTimeComparator(descending);
} else if (QUIZ_ATTR_END_TIME.equals(sortParam)) {
return getEndTimeComparator(descending);
}
return getIdComparator(descending);
}
} }

View file

@ -11,6 +11,8 @@ package ch.ethz.seb.sebserver.gbl.model.institution;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size; import javax.validation.constraints.Size;
import org.hibernate.validator.constraints.URL;
import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@ -26,11 +28,11 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.GrantEntity;
public final class LmsSetup implements GrantEntity, Activatable { public final class LmsSetup implements GrantEntity, Activatable {
public static final String FILTER_ATTR_LMS_SETUP = "lms_setup";
public static final String FILTER_ATTR_LMS_TYPE = "lms_type"; public static final String FILTER_ATTR_LMS_TYPE = "lms_type";
public enum LmsType { public enum LmsType {
MOCKUP, MOCKUP,
MOODLE,
OPEN_EDX OPEN_EDX
} }
@ -51,14 +53,13 @@ public final class LmsSetup implements GrantEntity, Activatable {
public final LmsType lmsType; public final LmsType lmsType;
@JsonProperty(LMS_SETUP.ATTR_LMS_CLIENTNAME) @JsonProperty(LMS_SETUP.ATTR_LMS_CLIENTNAME)
@Size(min = 3, max = 255, message = "lmsSetup:lmsClientname:size:{min}:{max}:${validatedValue}")
public final String lmsAuthName; public final String lmsAuthName;
@JsonProperty(LMS_SETUP.ATTR_LMS_CLIENTSECRET) @JsonProperty(LMS_SETUP.ATTR_LMS_CLIENTSECRET)
@Size(min = 8, max = 255, message = "lmsSetup:lmsClientsecret:size:{min}:{max}:${validatedValue}")
public final String lmsAuthSecret; public final String lmsAuthSecret;
@JsonProperty(LMS_SETUP.ATTR_LMS_URL) @JsonProperty(LMS_SETUP.ATTR_LMS_URL)
@URL(message = "lmsSetup:lmsUrl:invalidURL")
public final String lmsApiUrl; public final String lmsApiUrl;
@JsonProperty(LMS_SETUP.ATTR_LMS_REST_API_TOKEN) @JsonProperty(LMS_SETUP.ATTR_LMS_REST_API_TOKEN)

View file

@ -11,12 +11,14 @@ package ch.ethz.seb.sebserver.gbl.model.institution;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.Set; import java.util.List;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.util.Utils; import ch.ethz.seb.sebserver.gbl.util.Utils;
public final class LmsSetupTestResult { public final class LmsSetupTestResult {
@ -31,32 +33,37 @@ public final class LmsSetupTestResult {
public final Boolean okStatus; public final Boolean okStatus;
@JsonProperty(ATTR_MISSING_ATTRIBUTE) @JsonProperty(ATTR_MISSING_ATTRIBUTE)
public final Set<String> missingLMSSetupAttribute; public final List<APIMessage> missingLMSSetupAttribute;
@JsonProperty(ATTR_MISSING_ATTRIBUTE) @JsonProperty(ATTR_ERROR_TOKEN_REQUEST)
public final String tokenRequestError; public final String tokenRequestError;
@JsonProperty(ATTR_MISSING_ATTRIBUTE) @JsonProperty(ATTR_ERROR_QUIZ_REQUEST)
public final String quizRequestError; public final String quizRequestError;
public LmsSetupTestResult( public LmsSetupTestResult(
@JsonProperty(value = ATTR_OK_STATUS, required = true) final Boolean ok, @JsonProperty(value = ATTR_OK_STATUS, required = true) final Boolean ok,
@JsonProperty(ATTR_MISSING_ATTRIBUTE) final Collection<String> missingLMSSetupAttribute, @JsonProperty(ATTR_MISSING_ATTRIBUTE) final Collection<APIMessage> missingLMSSetupAttribute,
@JsonProperty(ATTR_MISSING_ATTRIBUTE) final String tokenRequestError, @JsonProperty(ATTR_ERROR_TOKEN_REQUEST) final String tokenRequestError,
@JsonProperty(ATTR_MISSING_ATTRIBUTE) final String quizRequestError) { @JsonProperty(ATTR_ERROR_QUIZ_REQUEST) final String quizRequestError) {
this.okStatus = ok; this.okStatus = ok;
// TODO // TODO
this.missingLMSSetupAttribute = Utils.immutableSetOf(missingLMSSetupAttribute); this.missingLMSSetupAttribute = Utils.immutableListOf(missingLMSSetupAttribute);
this.tokenRequestError = tokenRequestError; this.tokenRequestError = tokenRequestError;
this.quizRequestError = quizRequestError; this.quizRequestError = quizRequestError;
} }
@JsonIgnore
public boolean isOk() {
return this.okStatus != null && this.okStatus.booleanValue();
}
public Boolean getOkStatus() { public Boolean getOkStatus() {
return this.okStatus; return this.okStatus;
} }
public Set<String> getMissingLMSSetupAttribute() { public List<APIMessage> getMissingLMSSetupAttribute() {
return this.missingLMSSetupAttribute; return this.missingLMSSetupAttribute;
} }
@ -79,11 +86,11 @@ public final class LmsSetupTestResult {
return new LmsSetupTestResult(true, Collections.emptyList(), null, null); return new LmsSetupTestResult(true, Collections.emptyList(), null, null);
} }
public static final LmsSetupTestResult ofMissingAttributes(final Collection<String> attrs) { public static final LmsSetupTestResult ofMissingAttributes(final Collection<APIMessage> attrs) {
return new LmsSetupTestResult(false, attrs, null, null); return new LmsSetupTestResult(false, attrs, null, null);
} }
public static final LmsSetupTestResult ofMissingAttributes(final String... attrs) { public static final LmsSetupTestResult ofMissingAttributes(final APIMessage... attrs) {
if (attrs == null) { if (attrs == null) {
return new LmsSetupTestResult(false, Collections.emptyList(), null, null); return new LmsSetupTestResult(false, Collections.emptyList(), null, null);
} }

View file

@ -13,6 +13,9 @@ import java.util.function.Function;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** A result of a computation that can either be the resulting value of the computation /** A result of a computation that can either be the resulting value of the computation
* or an error if an exception/error has been thrown during the computation. * or an error if an exception/error has been thrown during the computation.
* *
@ -49,6 +52,8 @@ import java.util.stream.Stream;
* @param <T> The type of the result value */ * @param <T> The type of the result value */
public final class Result<T> { public final class Result<T> {
private static final Logger log = LoggerFactory.getLogger(Result.class);
/** The resulting value. May be null if an error occurred */ /** The resulting value. May be null if an error occurred */
private final T value; private final T value;
/** The error when happened otherwise null */ /** The error when happened otherwise null */
@ -270,6 +275,15 @@ public final class Result<T> {
} }
} }
public static <T> Stream<T> onErrorLogAndSkip(final Result<T> result) {
if (result.error != null) {
log.error("Unexpected error on result. Cause: ", result.error);
return Stream.empty();
} else {
return Stream.of(result.value);
}
}
@Override @Override
public int hashCode() { public int hashCode() {
final int prime = 31; final int prime = 31;

View file

@ -0,0 +1,42 @@
/*
* Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.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());
}
}

View file

@ -155,4 +155,10 @@ public final class Utils {
} }
} }
public static final String formatHTMLLines(final String message) {
return (message != null)
? message.replace("\n", "<br/>")
: null;
}
} }

View file

@ -174,7 +174,7 @@ public class InstitutionForm implements TemplateComposer {
.publishIf(() -> writeGrant && isReadonly && !institution.isActive()) .publishIf(() -> writeGrant && isReadonly && !institution.isActive())
.createAction(ActionDefinition.INSTITUTION_SAVE) .createAction(ActionDefinition.INSTITUTION_SAVE)
.withExec(formHandle::postChanges) .withExec(formHandle::processFormSave)
.publishIf(() -> !isReadonly) .publishIf(() -> !isReadonly)
.createAction(ActionDefinition.INSTITUTION_CANCEL_MODIFY) .createAction(ActionDefinition.INSTITUTION_CANCEL_MODIFY)

View file

@ -10,6 +10,7 @@ package ch.ethz.seb.sebserver.gui.content;
import java.util.function.BooleanSupplier; import java.util.function.BooleanSupplier;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Composite;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -21,9 +22,11 @@ import ch.ethz.seb.sebserver.gbl.model.Domain;
import ch.ethz.seb.sebserver.gbl.model.EntityKey; import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.model.user.UserInfo; import ch.ethz.seb.sebserver.gbl.model.user.UserInfo;
import ch.ethz.seb.sebserver.gbl.model.user.UserRole; import ch.ethz.seb.sebserver.gbl.model.user.UserRole;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition; import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition;
import ch.ethz.seb.sebserver.gui.form.FormBuilder; import ch.ethz.seb.sebserver.gui.form.FormBuilder;
import ch.ethz.seb.sebserver.gui.form.FormHandle; import ch.ethz.seb.sebserver.gui.form.FormHandle;
@ -31,6 +34,7 @@ import ch.ethz.seb.sebserver.gui.form.PageFormService;
import ch.ethz.seb.sebserver.gui.service.ResourceService; import ch.ethz.seb.sebserver.gui.service.ResourceService;
import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
import ch.ethz.seb.sebserver.gui.service.page.PageContext; import ch.ethz.seb.sebserver.gui.service.page.PageContext;
import ch.ethz.seb.sebserver.gui.service.page.PageMessageException;
import ch.ethz.seb.sebserver.gui.service.page.PageUtils; import ch.ethz.seb.sebserver.gui.service.page.PageUtils;
import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer; import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer;
import ch.ethz.seb.sebserver.gui.service.page.action.Action; import ch.ethz.seb.sebserver.gui.service.page.action.Action;
@ -39,6 +43,7 @@ import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.institution.GetIn
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.lmssetup.GetLmsSetup; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.lmssetup.GetLmsSetup;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.lmssetup.NewLmsSetup; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.lmssetup.NewLmsSetup;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.lmssetup.SaveLmsSetup; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.lmssetup.SaveLmsSetup;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.lmssetup.TestLmsSetup;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser; import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser.EntityGrantCheck; import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser.EntityGrantCheck;
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory; import ch.ethz.seb.sebserver.gui.widget.WidgetFactory;
@ -126,6 +131,9 @@ public class LmsSetupForm implements TemplateComposer {
.putStaticValueIf(isNotNew, .putStaticValueIf(isNotNew,
Domain.LMS_SETUP.ATTR_INSTITUTION_ID, Domain.LMS_SETUP.ATTR_INSTITUTION_ID,
String.valueOf(lmsSetup.getInstitutionId())) String.valueOf(lmsSetup.getInstitutionId()))
.putStaticValueIf(isNotNew,
Domain.LMS_SETUP.ATTR_LMS_TYPE,
String.valueOf(lmsSetup.getLmsType()))
.addField(FormBuilder.singleSelection( .addField(FormBuilder.singleSelection(
Domain.LMS_SETUP.ATTR_INSTITUTION_ID, Domain.LMS_SETUP.ATTR_INSTITUTION_ID,
"sebserver.lmssetup.form.institution", "sebserver.lmssetup.form.institution",
@ -143,22 +151,21 @@ public class LmsSetupForm implements TemplateComposer {
(lmsType != null) ? lmsType.name() : null, (lmsType != null) ? lmsType.name() : null,
this.resourceService::lmsTypeResources) this.resourceService::lmsTypeResources)
.readonlyIf(isNotNew)) .readonlyIf(isNotNew))
.addField(FormBuilder.text( .addField(FormBuilder.text(
Domain.LMS_SETUP.ATTR_LMS_URL, Domain.LMS_SETUP.ATTR_LMS_URL,
"sebserver.lmssetup.form.url", "sebserver.lmssetup.form.url",
lmsSetup.getLmsApiUrl()) lmsSetup.getLmsApiUrl())
.withCondition(() -> isNotNew.getAsBoolean() && lmsType != LmsType.MOCKUP)) .withCondition(() -> isNotNew.getAsBoolean()))
.addField(FormBuilder.text( .addField(FormBuilder.text(
Domain.LMS_SETUP.ATTR_LMS_CLIENTNAME, Domain.LMS_SETUP.ATTR_LMS_CLIENTNAME,
"sebserver.lmssetup.form.clientname.lms", "sebserver.lmssetup.form.clientname.lms",
lmsSetup.getLmsAuthName()) lmsSetup.getLmsAuthName())
.withCondition(() -> isNotNew.getAsBoolean() && lmsType != LmsType.MOCKUP)) .withCondition(() -> isNotNew.getAsBoolean()))
.addField(FormBuilder.text( .addField(FormBuilder.text(
Domain.LMS_SETUP.ATTR_LMS_CLIENTSECRET, Domain.LMS_SETUP.ATTR_LMS_CLIENTSECRET,
"sebserver.lmssetup.form.secret.lms") "sebserver.lmssetup.form.secret.lms")
.asPasswordField() .asPasswordField()
.withCondition(() -> isNotNew.getAsBoolean() && lmsType != LmsType.MOCKUP)) .withCondition(() -> isNotNew.getAsBoolean()))
.buildFor((entityKey == null) .buildFor((entityKey == null)
? restService.getRestCall(NewLmsSetup.class) ? restService.getRestCall(NewLmsSetup.class)
@ -176,6 +183,11 @@ public class LmsSetupForm implements TemplateComposer {
.withEntityKey(entityKey) .withEntityKey(entityKey)
.publishIf(() -> modifyGrant && readonly && istitutionActive) .publishIf(() -> modifyGrant && readonly && istitutionActive)
.createAction(ActionDefinition.LMS_SETUP_TEST)
.withEntityKey(entityKey)
.withExec(action -> this.testLmsSetup(action, formHandle))
.publishIf(() -> modifyGrant && isNotNew.getAsBoolean() && istitutionActive)
.createAction(ActionDefinition.LMS_SETUP_DEACTIVATE) .createAction(ActionDefinition.LMS_SETUP_DEACTIVATE)
.withEntityKey(entityKey) .withEntityKey(entityKey)
.withExec(restService::activation) .withExec(restService::activation)
@ -188,7 +200,7 @@ public class LmsSetupForm implements TemplateComposer {
.publishIf(() -> writeGrant && readonly && istitutionActive && !lmsSetup.isActive()) .publishIf(() -> writeGrant && readonly && istitutionActive && !lmsSetup.isActive())
.createAction(ActionDefinition.LMS_SETUP_SAVE) .createAction(ActionDefinition.LMS_SETUP_SAVE)
.withExec(formHandle::postChanges) .withExec(formHandle::processFormSave)
.publishIf(() -> !readonly) .publishIf(() -> !readonly)
.createAction(ActionDefinition.LMS_SETUP_CANCEL_MODIFY) .createAction(ActionDefinition.LMS_SETUP_CANCEL_MODIFY)
@ -199,4 +211,50 @@ public class LmsSetupForm implements TemplateComposer {
} }
/** LmsSetup test action implementation */
private Action testLmsSetup(final Action action, final FormHandle<LmsSetup> formHandle) {
// If we are in edit-mode we have to save the form before testing
if (!action.pageContext().isReadonly()) {
final Result<LmsSetup> postResult = formHandle.doAPIPost();
if (postResult.hasError()) {
formHandle.handleError(postResult.getError());
postResult.getOrThrow();
}
}
// Call the testing endpoint with the specified data to test
final EntityKey entityKey = action.getEntityKey();
final RestService restService = this.resourceService.getRestService();
final Result<LmsSetupTestResult> result = restService.getBuilder(TestLmsSetup.class)
.withURIVariable(API.PARAM_MODEL_ID, entityKey.getModelId())
.call();
// ... and handle the response
if (result.hasError()) {
if (formHandle.handleError(result.getError())) {
throw new PageMessageException(
new LocTextKey("sebserver.lmssetup.action.test.missingParameter"));
}
}
final LmsSetupTestResult testResult = result.getOrThrow();
if (testResult.isOk()) {
action.pageContext().publishInfo(
new LocTextKey("sebserver.lmssetup.action.test.ok"));
return action;
} else if (StringUtils.isNoneBlank(testResult.tokenRequestError)) {
throw new PageMessageException(
new LocTextKey("sebserver.lmssetup.action.test.tokenRequestError",
testResult.tokenRequestError));
} else if (StringUtils.isNoneBlank(testResult.quizRequestError)) {
throw new PageMessageException(
new LocTextKey("sebserver.lmssetup.action.test.quizRequestError", testResult.quizRequestError));
} else {
throw new PageMessageException(
new LocTextKey("sebserver.lmssetup.action.test.unknownError", testResult));
}
}
} }

View file

@ -105,7 +105,7 @@ public class UserAccountChangePasswordForm implements TemplateComposer {
pageContext.createAction(ActionDefinition.USER_ACCOUNT_CHANGE_PASSOWRD_SAVE) pageContext.createAction(ActionDefinition.USER_ACCOUNT_CHANGE_PASSOWRD_SAVE)
.withExec(action -> { .withExec(action -> {
formHandle.postChanges(action); formHandle.processFormSave(action);
if (ownAccount) { if (ownAccount) {
// NOTE: in this case the user changed the password of the own account // NOTE: in this case the user changed the password of the own account
// this should cause an logout with specified message that password change // this should cause an logout with specified message that password change

View file

@ -213,7 +213,7 @@ public class UserAccountForm implements TemplateComposer {
.createAction(ActionDefinition.USER_ACCOUNT_SAVE) .createAction(ActionDefinition.USER_ACCOUNT_SAVE)
.withExec(action -> { .withExec(action -> {
final Action postChanges = formHandle.postChanges(action); final Action postChanges = formHandle.processFormSave(action);
if (ownAccount) { if (ownAccount) {
currentUser.refresh(); currentUser.refresh();
pageContext.forwardToMainPage(); pageContext.forwardToMainPage();

View file

@ -182,6 +182,11 @@ public enum ActionDefinition {
ImageIcon.EDIT, ImageIcon.EDIT,
LmsSetupForm.class, LmsSetupForm.class,
LMS_SETUP_VIEW_LIST, false), LMS_SETUP_VIEW_LIST, false),
LMS_SETUP_TEST(
new LocTextKey("sebserver.lmssetup.action.test"),
ImageIcon.TEST,
LmsSetupForm.class,
LMS_SETUP_VIEW_LIST),
LMS_SETUP_CANCEL_MODIFY( LMS_SETUP_CANCEL_MODIFY(
new LocTextKey("sebserver.overall.action.modify.cancel"), new LocTextKey("sebserver.overall.action.modify.cancel"),
ImageIcon.CANCEL, ImageIcon.CANCEL,
@ -213,13 +218,13 @@ public enum ActionDefinition {
public final Class<? extends RestCall<?>> restCallType; public final Class<? extends RestCall<?>> restCallType;
public final ActionDefinition activityAlias; public final ActionDefinition activityAlias;
public final String category; public final String category;
public final boolean readonly; public final Boolean readonly;
private ActionDefinition( private ActionDefinition(
final LocTextKey title, final LocTextKey title,
final Class<? extends TemplateComposer> contentPaneComposer) { final Class<? extends TemplateComposer> contentPaneComposer) {
this(title, null, contentPaneComposer, ActionPane.class, null, null, null, true); this(title, null, contentPaneComposer, ActionPane.class, null, null, null, null);
} }
private ActionDefinition( private ActionDefinition(
@ -227,7 +232,7 @@ public enum ActionDefinition {
final Class<? extends TemplateComposer> contentPaneComposer, final Class<? extends TemplateComposer> contentPaneComposer,
final ActionDefinition activityAlias) { final ActionDefinition activityAlias) {
this(title, null, contentPaneComposer, ActionPane.class, null, activityAlias, null, true); this(title, null, contentPaneComposer, ActionPane.class, null, activityAlias, null, null);
} }
private ActionDefinition( private ActionDefinition(
@ -236,7 +241,7 @@ public enum ActionDefinition {
final Class<? extends TemplateComposer> contentPaneComposer, final Class<? extends TemplateComposer> contentPaneComposer,
final ActionDefinition activityAlias) { final ActionDefinition activityAlias) {
this(title, icon, contentPaneComposer, ActionPane.class, null, activityAlias, null, true); this(title, icon, contentPaneComposer, ActionPane.class, null, activityAlias, null, null);
} }
private ActionDefinition( private ActionDefinition(
@ -246,7 +251,7 @@ public enum ActionDefinition {
final Class<? extends RestCall<?>> restCallType, final Class<? extends RestCall<?>> restCallType,
final ActionDefinition activityAlias) { final ActionDefinition activityAlias) {
this(title, icon, contentPaneComposer, ActionPane.class, restCallType, activityAlias, null, true); this(title, icon, contentPaneComposer, ActionPane.class, restCallType, activityAlias, null, null);
} }
private ActionDefinition( private ActionDefinition(
@ -267,7 +272,7 @@ public enum ActionDefinition {
final Class<? extends RestCall<?>> restCallType, final Class<? extends RestCall<?>> restCallType,
final ActionDefinition activityAlias, final ActionDefinition activityAlias,
final String category, final String category,
final boolean readonly) { final Boolean readonly) {
this.title = title; this.title = title;
this.icon = icon; this.icon = icon;

View file

@ -162,6 +162,15 @@ public final class Form implements FormBinding {
} }
} }
public boolean hasAnyError() {
return this.formFields.entrySet()
.stream()
.flatMap(entity -> entity.getValue().stream())
.filter(a -> a.hasError)
.findFirst()
.isPresent();
}
public void process( public void process(
final Predicate<String> nameFilter, final Predicate<String> nameFilter,
final Consumer<FormFieldAccessor> processor) { final Consumer<FormFieldAccessor> processor) {

View file

@ -13,9 +13,9 @@ import java.util.function.Consumer;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.model.Entity; import ch.ethz.seb.sebserver.gbl.model.Entity;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition;
import ch.ethz.seb.sebserver.gui.form.Form.FormFieldAccessor; import ch.ethz.seb.sebserver.gui.form.Form.FormFieldAccessor;
import ch.ethz.seb.sebserver.gui.service.i18n.I18nSupport; import ch.ethz.seb.sebserver.gui.service.i18n.I18nSupport;
import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
@ -50,12 +50,24 @@ public class FormHandle<T extends Entity> {
this.i18nSupport = i18nSupport; this.i18nSupport = i18nSupport;
} }
public final Action postChanges(final Action action) { /** Process an API post request to send and save the form field values
return doAPIPost(action.definition) * to the webservice and publishes a page event to return to read-only-view
.getOrThrow(); * to indicate that the data was successfully saved or process an validation
* error indication if there are some validation errors.
*
* @param action the save action context
* @return the new Action context for read-only-view */
public final Action processFormSave(final Action action) {
return handleFormPost(doAPIPost(), action);
} }
public Result<Action> doAPIPost(final ActionDefinition actionDefinition) { /** process a form post by first resetting all field validation errors (if there are some)
* then collecting all input data from the form by form-binding to a either a JSON string in
* HTTP PUT case or to an form-URL-encoded string on HTTP POST case. And PUT or POST the data
* to the webservice by using the defined RestCall and return the response result of the RestCall.
*
* @return the response result of the post (or put) RestCall */
public Result<T> doAPIPost() {
this.form.process( this.form.process(
name -> true, name -> true,
fieldAccessor -> fieldAccessor.resetError()); fieldAccessor -> fieldAccessor.resetError());
@ -63,34 +75,52 @@ public class FormHandle<T extends Entity> {
return this.post return this.post
.newBuilder() .newBuilder()
.withFormBinding(this.form) .withFormBinding(this.form)
.call() .call();
.map(result -> {
final Action action = this.pageContext.createAction(actionDefinition)
.withAttribute(AttributeKeys.READ_ONLY, "true")
.withEntityKey(result.getEntityKey());
this.pageContext.publishPageEvent(new ActionEvent(action, false));
return action;
})
.onErrorDo(this::handleError)
//.map(this.postPostHandle)
;
} }
private void handleError(final Throwable error) { /** Uses the result of a form post to either create and publish a new Action to
* go to the read-only-view of the specified form to indicate a successful form post
* or stay within the edit-mode of the form and indicate errors or field validation messages
* to the user on error case.
*
* @param postResult The form post result
* @param action the action that was applied with the form post
* @return the new Action that was used to stay on page or go the read-only-view of the form */
public Action handleFormPost(final Result<T> postResult, final Action action) {
return postResult
.map(result -> {
final Action resultAction = action.createNew()
.withAttribute(AttributeKeys.READ_ONLY, "true")
.withEntityKey(result.getEntityKey());
action.pageContext().publishPageEvent(new ActionEvent(resultAction, false));
return resultAction;
})
.onErrorDo(this::handleError)
.getOrThrow();
}
public boolean handleError(final Throwable error) {
if (error instanceof RestCallError) { if (error instanceof RestCallError) {
((RestCallError) error) ((RestCallError) error)
.getErrorMessages() .getErrorMessages()
.stream() .stream()
.filter(APIMessage.ErrorMessage.FIELD_VALIDATION::isOf)
.map(FieldValidationError::new) .map(FieldValidationError::new)
.forEach(fve -> this.form.process( .forEach(fve -> this.form.process(
name -> name.equals(fve.fieldName), name -> name.equals(fve.fieldName),
fieldAccessor -> showValidationError(fieldAccessor, fve))); fieldAccessor -> showValidationError(fieldAccessor, fve)));
return true;
} else { } else {
log.error("Unexpected error while trying to post form: ", error); log.error("Unexpected error while trying to post form: ", error);
this.pageContext.notifyError(error); this.pageContext.notifyError(error);
return false;
} }
} }
public boolean hasAnyError() {
return this.form.hasAnyError();
}
private final void showValidationError( private final void showValidationError(
final FormFieldAccessor fieldAccessor, final FormFieldAccessor fieldAccessor,
final FieldValidationError valError) { final FieldValidationError valError) {

View file

@ -206,6 +206,14 @@ public interface PageContext {
* @param message the localized text key of the message */ * @param message the localized text key of the message */
void publishPageMessage(LocTextKey title, LocTextKey message); void publishPageMessage(LocTextKey title, LocTextKey message);
/** Publish an information message to the user with the given localized message.
* The message text can also be HTML text as far as RWT supports it
*
* @param message the localized text key of the message */
default void publishInfo(final LocTextKey message) {
publishPageMessage(new LocTextKey("sebserver.page.message"), message);
}
/** Publish and shows a formatted PageMessageException to the user. /** Publish and shows a formatted PageMessageException to the user.
* *
* @param pme the PageMessageException */ * @param pme the PageMessageException */

View file

@ -48,9 +48,12 @@ public final class Action implements Runnable {
this.definition = definition; this.definition = definition;
this.originalPageContext = pageContext; this.originalPageContext = pageContext;
final String readonly = pageContext.getAttribute(AttributeKeys.READ_ONLY, "true");
this.pageContext = pageContext.withAttribute( this.pageContext = pageContext.withAttribute(
AttributeKeys.READ_ONLY, AttributeKeys.READ_ONLY,
String.valueOf(definition.readonly)); definition.readonly != null
? String.valueOf(definition.readonly)
: readonly);
} }
@Override @Override
@ -89,6 +92,10 @@ public final class Action implements Runnable {
} }
} }
public Action createNew() {
return this.pageContext.createAction(this.definition);
}
public Action withExec(final Function<Action, Action> exec) { public Action withExec(final Function<Action, Action> exec) {
this.exec = exec; this.exec = exec;
return this; return this;

View file

@ -313,7 +313,7 @@ public class PageContextImpl implements PageContext {
final MessageBox messageBox = new Message( final MessageBox messageBox = new Message(
getShell(), getShell(),
this.i18nSupport.getText("sebserver.error.unexpected"), this.i18nSupport.getText("sebserver.error.unexpected"),
error.toString(), Utils.formatHTMLLines(errorMessage),
SWT.ERROR); SWT.ERROR);
messageBox.open(null); messageBox.open(null);
} }

View file

@ -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);
}
}

View file

@ -61,6 +61,7 @@ public class WidgetFactory {
MAXIMIZE("maximize.png"), MAXIMIZE("maximize.png"),
MINIMIZE("minimize.png"), MINIMIZE("minimize.png"),
EDIT("edit.png"), EDIT("edit.png"),
TEST("test.png"),
CANCEL("cancel.png"), CANCEL("cancel.png"),
CANCEL_EDIT("cancelEdit.png"), CANCEL_EDIT("cancelEdit.png"),
SHOW("show.png"), SHOW("show.png"),

View file

@ -23,16 +23,29 @@ import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
/** The MyBatis - Spring configuration
*
* All mapper- and model-classes in the specified sub-packages
* are auto-generated from DB schema by an external generator
*
* MyBatis is used on the lowest data - layer as an OR-Mapper with great flexibility and a good
* SQL builder interface.
*
* The Datasource is auto-configured by Spring and depends on the Spring property configuration so far */
@Configuration @Configuration
@MapperScan(basePackages = "ch.ethz.seb.sebserver.webservice.datalayer.batis") @MapperScan(basePackages = "ch.ethz.seb.sebserver.webservice.datalayer.batis")
@WebServiceProfile @WebServiceProfile
@Import(DataSourceAutoConfiguration.class) @Import(DataSourceAutoConfiguration.class)
public class BatisConfig { public class BatisConfig {
/** Name of the transaction manager bean for MyBatis based Spring controlled transactions */
public static final String TRANSACTION_MANAGER = "transactionManager"; public static final String TRANSACTION_MANAGER = "transactionManager";
/** Name of the sql session template bean of MyBatis */
public static final String SQL_SESSION_TEMPLATE = "sqlSessionTemplate"; public static final String SQL_SESSION_TEMPLATE = "sqlSessionTemplate";
/** Name of the sql session factory bean of MyBatis */
public static final String SQL_SESSION_FACTORY = "sqlSessionFactory"; public static final String SQL_SESSION_FACTORY = "sqlSessionFactory";
/** Transaction manager bean for MyBatis based Spring controlled transactions */
@Lazy @Lazy
@Bean(name = SQL_SESSION_FACTORY) @Bean(name = SQL_SESSION_FACTORY)
public SqlSessionFactory sqlSessionFactory(final DataSource dataSource) throws Exception { public SqlSessionFactory sqlSessionFactory(final DataSource dataSource) throws Exception {
@ -41,6 +54,7 @@ public class BatisConfig {
return factoryBean.getObject(); return factoryBean.getObject();
} }
/** SQL session template bean of MyBatis */
@Lazy @Lazy
@Bean(name = SQL_SESSION_TEMPLATE) @Bean(name = SQL_SESSION_TEMPLATE)
public SqlSessionTemplate sqlSessionTemplate(final DataSource dataSource) throws Exception { public SqlSessionTemplate sqlSessionTemplate(final DataSource dataSource) throws Exception {
@ -48,6 +62,7 @@ public class BatisConfig {
return sqlSessionTemplate; return sqlSessionTemplate;
} }
/** SQL session factory bean of MyBatis */
@Lazy @Lazy
@Bean(name = TRANSACTION_MANAGER) @Bean(name = TRANSACTION_MANAGER)
public DataSourceTransactionManager transactionManager(final DataSource dataSource) { public DataSourceTransactionManager transactionManager(final DataSource dataSource) {

View file

@ -229,6 +229,16 @@ public interface AuthorizationService {
return check(PrivilegeType.WRITE, grantEntity); return check(PrivilegeType.WRITE, grantEntity);
} }
/** Checks if the current user has role based view access to a specified user account.
*
* If user account has UserRole.SEB_SERVER_ADMIN this always gives true
* If user account has UserRole.INSTITUTIONAL_ADMIN this is true if the given user account has
* not the UserRole.SEB_SERVER_ADMIN (institutional administrators should not see SEB Server administrators)
* If the current user is the same as the given user account this is always true no matter if there are any
* user-account based privileges (every user shall see its own account)
*
* @param userAccount the user account the check role based view access
* @return true if the current user has role based view access to a specified user account */
default boolean hasRoleBasedUserAccountViewGrant(final UserInfo userAccount) { default boolean hasRoleBasedUserAccountViewGrant(final UserInfo userAccount) {
final EnumSet<UserRole> userRolesOfUserAccount = userAccount.getUserRoles(); final EnumSet<UserRole> userRolesOfUserAccount = userAccount.getUserRoles();
final SEBServerUser currentUser = getUserService().getCurrentUser(); final SEBServerUser currentUser = getUserService().getCurrentUser();

View file

@ -50,6 +50,10 @@ public interface UserService {
* @return an overall super user with all rights */ * @return an overall super user with all rights */
SEBServerUser getSuperUser(); SEBServerUser getSuperUser();
/** Binds the current users institution identifier as default value to a
*
* @RequestParam of type API.PARAM_INSTITUTION_ID if needed. See EntityController class for example
* @param binder Springs WebDataBinder is injected on controller side */
void addUsersInstitutionDefaultPropertySupport(final WebDataBinder binder); void addUsersInstitutionDefaultPropertySupport(final WebDataBinder binder);
} }

View file

@ -16,22 +16,30 @@ import java.util.LinkedHashSet;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.api.API.BulkActionType; import ch.ethz.seb.sebserver.gbl.api.API.BulkActionType;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.model.EntityKey; import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gbl.util.Utils; import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO.ActivityType; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO.ActivityType;
/** Defines a bulk action with its type, source entities (and source-type) and dependent entities.
* A BulkAction acts as a collector for entities (keys) that depends on the Bulk Action during the
* dependency collection phase.
* A BulkAction also acts as a result collector during the bulk-action process phase. */
public final class BulkAction { public final class BulkAction {
/** Defines the type of the BulkAction */
public final BulkActionType type; public final BulkActionType type;
/** Defines the EntityType of the source entities of the BulkAction */
public final EntityType sourceType; public final EntityType sourceType;
/** A Set of EntityKey defining all source-entities of the BulkAction */
public final Set<EntityKey> sources; public final Set<EntityKey> sources;
/** A Set of EntityKey containing collected depending entities during dependency collection and processing phase */
final Set<EntityKey> dependencies; final Set<EntityKey> dependencies;
/** A Set of EntityKey containing collected bulk action processing results during processing phase */
final Set<Result<EntityKey>> result; final Set<Result<EntityKey>> result;
/** Indicates if this BulkAction has already been processed and is not valid anymore */
boolean alreadyProcessed = false; boolean alreadyProcessed = false;
public BulkAction( public BulkAction(

View file

@ -8,176 +8,15 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction; package ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.stereotype.Service;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.EntityProcessingReport; import ch.ethz.seb.sebserver.gbl.model.EntityProcessingReport;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO.ActivityType;
@Service public interface BulkActionService {
@WebServiceProfile
public class BulkActionService {
private final Map<EntityType, BulkActionSupportDAO<?>> supporter; void collectDependencies(BulkAction action);
private final UserActivityLogDAO userActivityLogDAO;
public BulkActionService( Result<BulkAction> doBulkAction(BulkAction action);
final Collection<BulkActionSupportDAO<?>> supporter,
final UserActivityLogDAO userActivityLogDAO) {
this.supporter = new HashMap<>(); Result<EntityProcessingReport> createReport(BulkAction action);
for (final BulkActionSupportDAO<?> support : supporter) {
this.supporter.put(support.entityType(), support);
}
this.userActivityLogDAO = userActivityLogDAO;
}
public void collectDependencies(final BulkAction action) { }
checkProcessing(action);
for (final BulkActionSupportDAO<?> sup : this.supporter.values()) {
action.dependencies.addAll(sup.getDependencies(action));
}
action.alreadyProcessed = true;
}
public Result<BulkAction> doBulkAction(final BulkAction action) {
return Result.tryCatch(() -> {
checkProcessing(action);
final BulkActionSupportDAO<?> supportForSource = this.supporter
.get(action.sourceType);
if (supportForSource == null) {
action.alreadyProcessed = true;
throw new IllegalArgumentException("No bulk action support for: " + action);
}
collectDependencies(action);
if (!action.dependencies.isEmpty()) {
// process dependencies first...
final List<BulkActionSupportDAO<?>> dependancySupporter =
getDependancySupporter(action);
for (final BulkActionSupportDAO<?> support : dependancySupporter) {
action.result.addAll(support.processBulkAction(action));
}
}
action.result.addAll(supportForSource.processBulkAction(action));
processUserActivityLog(action);
action.alreadyProcessed = true;
return action;
});
}
public Result<EntityProcessingReport> createReport(final BulkAction action) {
if (!action.alreadyProcessed) {
return doBulkAction(action)
.flatMap(this::createFullReport);
} else {
return createFullReport(action);
}
}
private Result<EntityProcessingReport> createFullReport(final BulkAction action) {
return Result.tryCatch(() -> {
// TODO
return new EntityProcessingReport(
action.sources,
Collections.emptyList(),
Collections.emptyList());
});
}
private void processUserActivityLog(final BulkAction action) {
final ActivityType activityType = action.getActivityType();
if (activityType == null) {
return;
}
for (final EntityKey key : action.dependencies) {
this.userActivityLogDAO.log(
activityType,
key.entityType,
key.modelId,
"bulk action dependency");
}
for (final EntityKey key : action.sources) {
this.userActivityLogDAO.log(
activityType,
key.entityType,
key.modelId,
"bulk action source");
}
}
private List<BulkActionSupportDAO<?>> getDependancySupporter(final BulkAction action) {
switch (action.type) {
case ACTIVATE:
case DEACTIVATE:
case HARD_DELETE: {
final List<BulkActionSupportDAO<?>> dependantSupporterInHierarchicalOrder =
getDependantSupporterInHierarchicalOrder(action);
Collections.reverse(dependantSupporterInHierarchicalOrder);
return dependantSupporterInHierarchicalOrder
.stream()
.filter(v -> v != null)
.collect(Collectors.toList());
}
default:
return getDependantSupporterInHierarchicalOrder(action);
}
}
private List<BulkActionSupportDAO<?>> getDependantSupporterInHierarchicalOrder(final BulkAction action) {
switch (action.sourceType) {
case INSTITUTION:
return Arrays.asList(
this.supporter.get(EntityType.LMS_SETUP),
this.supporter.get(EntityType.USER),
this.supporter.get(EntityType.EXAM),
this.supporter.get(EntityType.INDICATOR),
this.supporter.get(EntityType.CLIENT_CONNECTION),
this.supporter.get(EntityType.CONFIGURATION_NODE));
case USER:
return Arrays.asList(
this.supporter.get(EntityType.EXAM),
this.supporter.get(EntityType.INDICATOR),
this.supporter.get(EntityType.CLIENT_CONNECTION),
this.supporter.get(EntityType.CONFIGURATION_NODE));
case LMS_SETUP:
case EXAM:
case CONFIGURATION:
return Arrays.asList(
this.supporter.get(EntityType.EXAM),
this.supporter.get(EntityType.INDICATOR),
this.supporter.get(EntityType.CLIENT_CONNECTION));
default:
return Collections.emptyList();
}
}
private void checkProcessing(final BulkAction action) {
if (action.alreadyProcessed) {
throw new IllegalStateException("Given BulkAction has already been processed. Use a new one");
}
}
}

View file

@ -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");
}
}
}

View file

@ -25,6 +25,10 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ActivatableEntityDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.DAOLoggingSupport; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.DAOLoggingSupport;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.EntityDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.EntityDAO;
/** Defines overall DAO support for bulk-actions like activate, deactivate, delete...
*
*
* @param <T> The type of the Entity of a concrete BulkActionSupportDAO */
public interface BulkActionSupportDAO<T extends Entity> { public interface BulkActionSupportDAO<T extends Entity> {
/** Get the entity type for a concrete EntityDAO implementation. /** Get the entity type for a concrete EntityDAO implementation.
@ -32,8 +36,21 @@ public interface BulkActionSupportDAO<T extends Entity> {
* @return The EntityType for a concrete EntityDAO implementation */ * @return The EntityType for a concrete EntityDAO implementation */
EntityType entityType(); EntityType entityType();
/** Gets a Set of EntityKey for all dependent entities for a given BulkAction
* and the type of this BulkActionSupportDAO.
*
* @param bulkAction the BulkAction to get keys of dependencies for the concrete type of this BulkActionSupportDAO
* @return */
Set<EntityKey> getDependencies(BulkAction bulkAction); Set<EntityKey> getDependencies(BulkAction bulkAction);
/** This processed a given BulkAction for all entities of the concrete type of this BulkActionSupportDAO
* that are defined by this given BulkAction.
*
* This returns a Collection of EntityKey results of each Entity that has been processed.
* If there was an error for a particular Entity, the Result will have an error reference.
*
* @param bulkAction the BulkAction containing the source entity and all dependencies
* @return a Collection of EntityKey results of each Entity that has been processed. */
@Transactional @Transactional
default Collection<Result<EntityKey>> processBulkAction(final BulkAction bulkAction) { default Collection<Result<EntityKey>> processBulkAction(final BulkAction bulkAction) {
final Set<EntityKey> all = bulkAction.extractKeys(entityType()); final Set<EntityKey> all = bulkAction.extractKeys(entityType());

View file

@ -8,178 +8,30 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.client; package ch.ethz.seb.sebserver.webservice.servicelayer.client;
import java.io.UnsupportedEncodingException;
import java.security.SecureRandom;
import org.apache.commons.lang3.RandomStringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.env.Environment;
import org.springframework.security.crypto.codec.Hex;
import org.springframework.security.crypto.encrypt.Encryptors;
import org.springframework.stereotype.Service;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
@Lazy public interface ClientCredentialService {
@Service
@WebServiceProfile
public class ClientCredentialService {
private static final Logger log = LoggerFactory.getLogger(ClientCredentialService.class); Result<ClientCredentials> createGeneratedClientCredentials();
static final String SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY = "sebserver.webservice.internalSecret"; Result<ClientCredentials> createGeneratedClientCredentials(CharSequence salt);
static final CharSequence DEFAULT_SALT = "b7dbe99bbfa3e21e";
private final Environment environment; ClientCredentials encryptedClientCredentials(ClientCredentials clientCredentials);
protected ClientCredentialService(final Environment environment) { ClientCredentials encryptedClientCredentials(
this.environment = environment; ClientCredentials clientCredentials,
} CharSequence salt);
public Result<ClientCredentials> createGeneratedClientCredentials() { CharSequence getPlainClientId(ClientCredentials credentials);
return createGeneratedClientCredentials(null);
}
public Result<ClientCredentials> createGeneratedClientCredentials(final CharSequence salt) { CharSequence getPlainClientId(ClientCredentials credentials, CharSequence salt);
return Result.tryCatch(() -> {
try { CharSequence getPlainClientSecret(ClientCredentials credentials);
return encryptedClientCredentials(
new ClientCredentials(
generateClientId().toString(),
generateClientSecret().toString(),
null),
salt);
} catch (final UnsupportedEncodingException e) {
log.error("Error while trying to generate client credentials: ", e);
throw new RuntimeException("cause: ", e);
}
});
}
public ClientCredentials encryptedClientCredentials(final ClientCredentials clientCredentials) { CharSequence getPlainClientSecret(ClientCredentials credentials, CharSequence salt);
return encryptedClientCredentials(clientCredentials, null);
}
public ClientCredentials encryptedClientCredentials( CharSequence getPlainAccessToken(ClientCredentials credentials);
final ClientCredentials clientCredentials,
final CharSequence salt) {
final CharSequence secret = this.environment CharSequence getPlainAccessToken(ClientCredentials credentials, CharSequence salt);
.getRequiredProperty(SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY);
return new ClientCredentials( }
(clientCredentials.clientId != null)
? encrypt(clientCredentials.clientId, secret, salt).toString()
: null,
(clientCredentials.secret != null)
? encrypt(clientCredentials.secret, secret, salt).toString()
: null,
(clientCredentials.accessToken != null)
? encrypt(clientCredentials.accessToken, secret, salt).toString()
: null);
}
public CharSequence getPlainClientId(final ClientCredentials credentials) {
return getPlainClientId(credentials, null);
}
public CharSequence getPlainClientId(final ClientCredentials credentials, final CharSequence salt) {
if (credentials == null || credentials.clientId == null) {
return null;
}
final CharSequence secret = this.environment
.getRequiredProperty(SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY);
return this.decrypt(credentials.clientId, secret, salt);
}
public CharSequence getPlainClientSecret(final ClientCredentials credentials) {
return getPlainClientSecret(credentials, null);
}
public CharSequence getPlainClientSecret(final ClientCredentials credentials, final CharSequence salt) {
if (credentials == null || credentials.secret == null) {
return null;
}
final CharSequence secret = this.environment
.getRequiredProperty(SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY);
return this.decrypt(credentials.secret, secret, salt);
}
public CharSequence getPlainAccessToken(final ClientCredentials credentials) {
return getPlainAccessToken(credentials, null);
}
public CharSequence getPlainAccessToken(final ClientCredentials credentials, final CharSequence salt) {
if (credentials == null || credentials.accessToken == null) {
return null;
}
final CharSequence secret = this.environment
.getRequiredProperty(SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY);
return this.decrypt(credentials.accessToken, secret, salt);
}
CharSequence encrypt(final CharSequence text, final CharSequence secret, final CharSequence salt) {
if (text == null) {
throw new IllegalArgumentException("Text has null reference");
}
try {
return Encryptors
.delux(secret, getSalt(salt))
.encrypt(text.toString());
} catch (final Exception e) {
log.error("Failed to encrypt text: ", e);
return text;
}
}
CharSequence decrypt(final CharSequence text, final CharSequence secret, final CharSequence salt) {
if (text == null) {
throw new IllegalArgumentException("Text has null reference");
}
try {
return Encryptors
.delux(secret, getSalt(salt))
.decrypt(text.toString());
} catch (final Exception e) {
log.error("Failed to decrypt text: ", e);
return text;
}
}
private final static char[] possibleCharacters = (new String(
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789~`!@#$%^&*()-_=+[{]}?"))
.toCharArray();
private CharSequence getSalt(final CharSequence saltPlain) throws UnsupportedEncodingException {
final CharSequence _salt = (saltPlain == null || saltPlain.length() <= 0)
? this.environment.getProperty(SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY, DEFAULT_SALT.toString())
: saltPlain;
return new String(Hex.encode(_salt.toString().getBytes("UTF-8")));
}
private CharSequence generateClientId() {
return RandomStringUtils.random(
16, 0, possibleCharacters.length - 1, false, false,
possibleCharacters, new SecureRandom());
}
private CharSequence generateClientSecret() throws UnsupportedEncodingException {
return RandomStringUtils.random(
64, 0, possibleCharacters.length - 1, false, false,
possibleCharacters, new SecureRandom());
}
}

View file

@ -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());
}
}

View file

@ -15,6 +15,7 @@ import org.slf4j.LoggerFactory;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
/** Adds some static logging support for DAO's */
public final class DAOLoggingSupport { public final class DAOLoggingSupport {
public static final Logger log = LoggerFactory.getLogger(DAOLoggingSupport.class); public static final Logger log = LoggerFactory.getLogger(DAOLoggingSupport.class);

View file

@ -13,8 +13,10 @@ import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap; import org.springframework.util.MultiValueMap;
import ch.ethz.seb.sebserver.gbl.api.POSTMapper; import ch.ethz.seb.sebserver.gbl.api.POSTMapper;
import ch.ethz.seb.sebserver.gbl.model.Entity;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam; import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.Indicator; import ch.ethz.seb.sebserver.gbl.model.exam.Indicator;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.institution.SebClientConfig; import ch.ethz.seb.sebserver.gbl.model.institution.SebClientConfig;
import ch.ethz.seb.sebserver.gbl.model.user.UserInfo; import ch.ethz.seb.sebserver.gbl.model.user.UserInfo;
@ -39,7 +41,7 @@ public class FilterMap extends POSTMapper {
} }
public String getName() { public String getName() {
return getSQLWildcard(UserInfo.FILTER_ATTR_NAME); return getSQLWildcard(Entity.FILTER_ATTR_NAME);
} }
public String getUserUsername() { public String getUserUsername() {
@ -54,14 +56,14 @@ public class FilterMap extends POSTMapper {
return getString(UserInfo.FILTER_ATTR_LANGUAGE); return getString(UserInfo.FILTER_ATTR_LANGUAGE);
} }
public String getLmsSetupName() {
return getSQLWildcard(LmsSetup.FILTER_ATTR_NAME);
}
public String getLmsSetupType() { public String getLmsSetupType() {
return getString(LmsSetup.FILTER_ATTR_LMS_TYPE); return getString(LmsSetup.FILTER_ATTR_LMS_TYPE);
} }
public DateTime getQuizFromTime() {
return JodaTimeTypeResolver.getDateTime(getString(QuizData.FILTER_ATTR_START_TIME));
}
public DateTime getExamFromTime() { public DateTime getExamFromTime() {
return JodaTimeTypeResolver.getDateTime(getString(Exam.FILTER_ATTR_FROM)); return JodaTimeTypeResolver.getDateTime(getString(Exam.FILTER_ATTR_FROM));
} }
@ -74,8 +76,8 @@ public class FilterMap extends POSTMapper {
return getString(Exam.FILTER_ATTR_QUIZ_ID); return getString(Exam.FILTER_ATTR_QUIZ_ID);
} }
public Long getExamLmsSetupId() { public Long getLmsSetupId() {
return getLong(Exam.FILTER_ATTR_LMS_SETUP); return getLong(LmsSetup.FILTER_ATTR_LMS_SETUP);
} }
public String getExamStatus() { public String getExamStatus() {

View file

@ -134,7 +134,7 @@ public class ExamDAOImpl implements ExamDAO {
isEqualToWhenPresent(filterMap.getExamQuizId())) isEqualToWhenPresent(filterMap.getExamQuizId()))
.and( .and(
ExamRecordDynamicSqlSupport.lmsSetupId, ExamRecordDynamicSqlSupport.lmsSetupId,
isEqualToWhenPresent(filterMap.getExamLmsSetupId())) isEqualToWhenPresent(filterMap.getLmsSetupId()))
.and( .and(
ExamRecordDynamicSqlSupport.status, ExamRecordDynamicSqlSupport.status,
isEqualToWhenPresent(filterMap.getExamStatus())) isEqualToWhenPresent(filterMap.getExamStatus()))
@ -366,7 +366,7 @@ public class ExamDAOImpl implements ExamDAO {
(map1, map2) -> Utils.mapPutAll(map1, map2)); (map1, map2) -> Utils.mapPutAll(map1, map2));
return this.lmsAPIService return this.lmsAPIService
.createLmsAPITemplate(lmsSetupId) .getLmsAPITemplate(lmsSetupId)
.map(template -> template.getQuizzes(recordMapping.keySet())) .map(template -> template.getQuizzes(recordMapping.keySet()))
.getOrThrow() .getOrThrow()
.stream() .stream()

View file

@ -110,7 +110,7 @@ public class LmsSetupDAOImpl implements LmsSetupDAO {
isEqualToWhenPresent(filterMap.getInstitutionId())) isEqualToWhenPresent(filterMap.getInstitutionId()))
.and( .and(
LmsSetupRecordDynamicSqlSupport.name, LmsSetupRecordDynamicSqlSupport.name,
isLikeWhenPresent(filterMap.getLmsSetupName())) isLikeWhenPresent(filterMap.getName()))
.and( .and(
LmsSetupRecordDynamicSqlSupport.lmsType, LmsSetupRecordDynamicSqlSupport.lmsType,
isEqualToWhenPresent(filterMap.getLmsSetupType())) isEqualToWhenPresent(filterMap.getLmsSetupType()))

View file

@ -8,13 +8,84 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.lms; package ch.ethz.seb.sebserver.webservice.servicelayer.lms;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; import java.util.List;
import ch.ethz.seb.sebserver.gbl.util.Result; import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import ch.ethz.seb.sebserver.gbl.model.Page;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
/** Defines the LMS API access service interface with all functionality needed to access
* a LMS API within a given LmsSetup configuration.
*
* There are LmsAPITemplate implementations for each type of supported LMS that are managed
* in reference to a LmsSetup configuration within this service. This means actually that
* this service caches requested LmsAPITemplate (that holds the LMS API connection) as long
* as there is no change in the underling LmsSetup configuration. If the LmsSetup configuration
* changes this service will be notifies about the change and release the related LmsAPITemplate from cache. */
public interface LmsAPIService { public interface LmsAPIService {
Result<LmsAPITemplate> createLmsAPITemplate(Long lmsSetupId); Result<Page<QuizData>> requestQuizDataPage(
final int pageNumber,
final int pageSize,
final String sort,
final FilterMap filterMap);
Result<LmsAPITemplate> createLmsAPITemplate(LmsSetup lmsSetup); /** Get a LmsAPITemplate for specified LmsSetup configuration.
*
* @param lmsSetupId the identifier of LmsSetup
* @return LmsAPITemplate for specified LmsSetup configuration */
Result<LmsAPITemplate> getLmsAPITemplate(String lmsSetupId);
default Result<LmsAPITemplate> getLmsAPITemplate(final Long lmsSetupId) {
if (lmsSetupId == null) {
return Result.ofError(new IllegalArgumentException("lmsSetupId has null-reference"));
}
return getLmsAPITemplate(String.valueOf(lmsSetupId));
}
public static Predicate<QuizData> quizzeFilterFunction(final FilterMap filterMap) {
final String name = filterMap.getName();
final DateTime from = filterMap.getQuizFromTime();
return q -> (StringUtils.isBlank(name) || (q.name != null && q.name.contains(name)))
&& (from == null) || (q.startTime != null && q.startTime.isBefore(from));
}
public static Function<List<QuizData>, List<QuizData>> quizzesFilterFunction(final FilterMap filterMap) {
filterMap.getName();
return quizzes -> quizzes
.stream()
.filter(quizzeFilterFunction(filterMap))
.collect(Collectors.toList());
}
public static Function<List<QuizData>, Page<QuizData>> quizzesToPageFunction(
final String sort,
final int pageNumber,
final int pageSize) {
return quizzes -> {
final int start = pageNumber * pageSize;
int end = start + pageSize;
if (end > quizzes.size() - 1) {
end = quizzes.size() - 1;
}
return new Page<>(quizzes.size() / pageSize, pageNumber, sort, quizzes.subList(start, end));
};
}
public static Function<List<QuizData>, List<QuizData>> quizzesSortFunction(final String sort) {
return quizzes -> {
quizzes.sort(QuizData.getComparator(sort));
return quizzes;
};
}
} }

View file

@ -8,33 +8,77 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.lms; package ch.ethz.seb.sebserver.webservice.servicelayer.lms;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List;
import java.util.Set; import java.util.Set;
import ch.ethz.seb.sebserver.gbl.model.Page; import org.apache.commons.lang3.StringUtils;
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.model.Domain.LMS_SETUP;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData; import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails; import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentials;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
/** Defines the interface to an LMS within a specified LMSSetup configuration.
* There is one concrete implementations for every supported type of LMS like
* Open edX or Moodle
*
* A LmsAPITemplate defines at least the core API access to query courses and quizzes from the LMS
* Later a concrete LmsAPITemplate may also implement some special features regarding to the type
* of LMS */
public interface LmsAPITemplate { public interface LmsAPITemplate {
Result<LmsSetup> lmsSetup(); /** Get the underling LMSSetup configuration for this LmsAPITemplate
*
* @return the underling LMSSetup configuration for this LmsAPITemplate */
LmsSetup lmsSetup();
/** Performs a test for the underling LmsSetup configuration and checks if the
* LMS and the core API of the LMS can be accessed or if there are some difficulties,
* missing configuration data or connection/authentication errors.
*
* @return LmsSetupTestResult instance with the test result report */
LmsSetupTestResult testLmsSetup(); LmsSetupTestResult testLmsSetup();
Result<Page<QuizData>> getQuizzes( /** Get a Result of an unsorted List of filtered QuizData from the LMS course/quiz API
String name, *
Long from, * @param filterMap the FilterMap to get a filtered result. For possible filter attributes
String sort, * see documentation on QuizData
int pageNumber, * @return Result of an unsorted List of filtered QuizData from the LMS course/quiz API
int pageSize); * or refer to an error when happened */
Result<List<QuizData>> getQuizzes(FilterMap filterMap);
Collection<Result<QuizData>> getQuizzes(Set<String> ids); Collection<Result<QuizData>> getQuizzes(Set<String> ids);
Result<ExamineeAccountDetails> getExamineeAccountDetails(String examineeUserId); Result<ExamineeAccountDetails> getExamineeAccountDetails(String examineeUserId);
void reset(); default List<APIMessage> attributeValidation(final ClientCredentials credentials) {
final LmsSetup lmsSetup = lmsSetup();
// validation of LmsSetup
final List<APIMessage> missingAttrs = new ArrayList<>();
if (StringUtils.isBlank(lmsSetup.lmsApiUrl)) {
missingAttrs.add(APIMessage.fieldValidationError(
LMS_SETUP.ATTR_LMS_URL,
"lmsSetup:lmsUrl:notNull"));
}
if (StringUtils.isBlank(credentials.clientId)) {
missingAttrs.add(APIMessage.fieldValidationError(
LMS_SETUP.ATTR_LMS_CLIENTNAME,
"lmsSetup:lmsClientname:notNull"));
}
if (StringUtils.isBlank(credentials.secret)) {
missingAttrs.add(APIMessage.fieldValidationError(
LMS_SETUP.ATTR_LMS_CLIENTSECRET,
"lmsSetup:lmsClientsecret:notNull"));
}
return missingAttrs;
}
} }

View file

@ -8,39 +8,57 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl; package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.event.EventListener;
import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import ch.ethz.seb.sebserver.gbl.Constants; import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.model.Page;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentialService; import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentialService;
import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentials;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.LmsSetupDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.LmsSetupDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
import ch.ethz.seb.sebserver.webservice.weblayer.api.IllegalAPIArgumentException;
@Lazy
@Service @Service
@WebServiceProfile @WebServiceProfile
public class LmsAPIServiceImpl implements LmsAPIService { public class LmsAPIServiceImpl implements LmsAPIService {
private static final Logger log = LoggerFactory.getLogger(LmsAPIServiceImpl.class);
private final LmsSetupDAO lmsSetupDAO; private final LmsSetupDAO lmsSetupDAO;
private final ClientCredentialService internalEncryptionService; private final ClientCredentialService clientCredentialService;
private final ClientHttpRequestFactory clientHttpRequestFactory; private final ClientHttpRequestFactory clientHttpRequestFactory;
private final String[] openEdxAlternativeTokenRequestPaths; private final String[] openEdxAlternativeTokenRequestPaths;
// TODO internal caching of LmsAPITemplate per LmsSetup (Id) private final Map<CacheKey, LmsAPITemplate> cache = new ConcurrentHashMap<>();
public LmsAPIServiceImpl( public LmsAPIServiceImpl(
final LmsSetupDAO lmsSetupDAO, final LmsSetupDAO lmsSetupDAO,
final ClientCredentialService internalEncryptionService, final ClientCredentialService clientCredentialService,
final ClientHttpRequestFactory clientHttpRequestFactory, final ClientHttpRequestFactory clientHttpRequestFactory,
@Value("${sebserver.lms.openedix.api.token.request.paths}") final String alternativeTokenRequestPaths) { @Value("${sebserver.lms.openedix.api.token.request.paths}") final String alternativeTokenRequestPaths) {
this.lmsSetupDAO = lmsSetupDAO; this.lmsSetupDAO = lmsSetupDAO;
this.internalEncryptionService = internalEncryptionService; this.clientCredentialService = clientCredentialService;
this.clientHttpRequestFactory = clientHttpRequestFactory; this.clientHttpRequestFactory = clientHttpRequestFactory;
this.openEdxAlternativeTokenRequestPaths = (alternativeTokenRequestPaths != null) this.openEdxAlternativeTokenRequestPaths = (alternativeTokenRequestPaths != null)
@ -48,60 +66,167 @@ public class LmsAPIServiceImpl implements LmsAPIService {
: null; : null;
} }
@Override /** Listen to LmsSetupChangeEvent to release an affected LmsAPITemplate from cache
public Result<LmsAPITemplate> createLmsAPITemplate(final Long lmsSetupId) { *
return this.lmsSetupDAO * @param event the event holding the changed LmsSetup */
.byPK(lmsSetupId) @EventListener
.flatMap(this::createLmsAPITemplate); public void notifyLmsSetupChange(final LmsSetupChangeEvent event) {
} final LmsSetup lmsSetup = event.getLmsSetup();
if (lmsSetup == null) {
@Override return;
public Result<LmsAPITemplate> createLmsAPITemplate(final LmsSetup lmsSetup) {
switch (lmsSetup.lmsType) {
case MOCKUP:
return Result.of(new MockupLmsAPITemplate(
this.lmsSetupDAO,
lmsSetup,
this.internalEncryptionService));
case OPEN_EDX:
return Result.of(new OpenEdxLmsAPITemplate(
lmsSetup.getModelId(),
this.lmsSetupDAO,
this.internalEncryptionService,
this.clientHttpRequestFactory,
this.openEdxAlternativeTokenRequestPaths));
default:
return Result.ofError(
new UnsupportedOperationException("No support for LMS Type: " + lmsSetup.lmsType));
} }
log.debug("LmsSetup changed. Update cache by removeing eventually used references");
this.cache.remove(new CacheKey(lmsSetup.getModelId(), 0));
} }
// @Override @Override
// public Result<InputStream> createSEBStartConfiguration(final Long lmsSetupId) { public Result<Page<QuizData>> requestQuizDataPage(
// return this.lmsSetupDAO final int pageNumber,
// .byPK(lmsSetupId) final int pageSize,
// .flatMap(this::createSEBStartConfiguration); final String sort,
// } final FilterMap filterMap) {
//
// @Override return getAllQuizzesFromLMSSetups(filterMap)
// public Result<InputStream> createSEBStartConfiguration(final LmsSetup lmsSetup) { .map(LmsAPIService.quizzesSortFunction(sort))
// .map(LmsAPIService.quizzesToPageFunction(sort, pageNumber, pageSize));
// // TODO implementation of creation of SEB start configuration for specified LmsSetup }
// // A SEB start configuration should at least contain the SEB-Client-Credentials to access the SEB Server API
// // and the SEB Server URL /** Collect all QuizData from all affecting LmsSetup.
// // * If filterMap contains a LmsSetup identifier, only the QuizData from that LmsSetup is collected.
// // To Clarify : The format of a SEB start configuration * Otherwise QuizData from all active LmsSetup of the current institution are collected.
// // To Clarify : How the file should be encrypted (use case) maybe we need another encryption-secret for this that can be given by *
// // an administrator on SEB start configuration creation time * @param filterMap the FilterMap containing either an LmsSetup identifier or an institution identifier
// * @return list of QuizData from all affecting LmsSetup */
// return Result.tryCatch(() -> { private Result<List<QuizData>> getAllQuizzesFromLMSSetups(final FilterMap filterMap) {
// try {
// return new ByteArrayInputStream("TODO".getBytes("UTF-8")); return Result.tryCatch(() -> {
// } catch (final UnsupportedEncodingException e) { final Long lmsSetupId = filterMap.getLmsSetupId();
// throw new RuntimeException("cause: ", e); if (lmsSetupId != null) {
// } return getLmsAPITemplate(lmsSetupId)
// }); .getOrThrow()
// } .getQuizzes(filterMap)
.getOrThrow();
}
final Long institutionId = filterMap.getInstitutionId();
if (institutionId == null) {
throw new IllegalAPIArgumentException("Missing institution identifier");
}
return this.lmsSetupDAO.all(institutionId, true)
.getOrThrow()
.stream()
.map(this::getLmsAPITemplate)
.flatMap(Result::onErrorLogAndSkip)
.map(template -> template.getQuizzes(filterMap))
.flatMap(Result::onErrorLogAndSkip)
.flatMap(List::stream)
.collect(Collectors.toList());
});
}
@Override
public Result<LmsAPITemplate> getLmsAPITemplate(final String lmsSetupId) {
log.debug("Get LmsAPITemplate for id: {}", lmsSetupId);
return Result.tryCatch(() -> {
return this.lmsSetupDAO
.byModelId(lmsSetupId)
.getOrThrow();
})
.flatMap(this::getLmsAPITemplate);
}
private Result<LmsAPITemplate> getLmsAPITemplate(final LmsSetup lmsSetup) {
return Result.tryCatch(() -> {
LmsAPITemplate lmsAPITemplate = getFromCache(lmsSetup);
if (lmsAPITemplate == null) {
log.debug("Get cached LmsAPITemplate with id: {}", lmsSetup.getModelId());
return lmsAPITemplate;
}
lmsAPITemplate = createLmsSetupTemplate(lmsSetup);
this.cache.put(new CacheKey(lmsSetup.getModelId(), System.currentTimeMillis()), lmsAPITemplate);
return lmsAPITemplate;
});
}
private LmsAPITemplate getFromCache(final LmsSetup lmsSetup) {
// first cleanup the cache by removing old instances
final long currentTimeMillis = System.currentTimeMillis();
new ArrayList<>(this.cache.keySet())
.stream()
.filter(key -> key.creationTimestamp - currentTimeMillis > Constants.DAY_IN_MILLIS)
.forEach(key -> this.cache.remove(key));
// get from cache
return this.cache.get(new CacheKey(lmsSetup.getModelId(), 0));
}
private LmsAPITemplate createLmsSetupTemplate(final LmsSetup lmsSetup) {
log.debug("Create new LmsAPITemplate for id: {}", lmsSetup.getModelId());
final ClientCredentials credentials = this.lmsSetupDAO
.getLmsAPIAccessCredentials(lmsSetup.getModelId())
.getOrThrow();
switch (lmsSetup.lmsType) {
case MOCKUP:
return new MockupLmsAPITemplate(
lmsSetup,
credentials,
this.clientCredentialService);
case OPEN_EDX:
return new OpenEdxLmsAPITemplate(
lmsSetup,
credentials,
this.clientCredentialService,
this.clientHttpRequestFactory,
this.openEdxAlternativeTokenRequestPaths);
default:
throw new UnsupportedOperationException("No support for LMS Type: " + lmsSetup.lmsType);
}
}
private static final class CacheKey {
final String lmsSetupId;
final long creationTimestamp;
final int hash;
CacheKey(final String lmsSetupId, final long creationTimestamp) {
this.lmsSetupId = lmsSetupId;
this.creationTimestamp = creationTimestamp;
final int prime = 31;
int result = 1;
result = prime * result + ((lmsSetupId == null) ? 0 : lmsSetupId.hashCode());
this.hash = result;
}
@Override
public int hashCode() {
return this.hash;
}
@Override
public boolean equals(final Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
final CacheKey other = (CacheKey) obj;
if (this.lmsSetupId == null) {
if (other.lmsSetupId != null)
return false;
} else if (!this.lmsSetupId.equals(other.lmsSetupId))
return false;
return true;
}
}
} }

View file

@ -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;
}
}

View file

@ -10,50 +10,46 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import ch.ethz.seb.sebserver.gbl.model.Domain.LMS_SETUP; import org.slf4j.Logger;
import ch.ethz.seb.sebserver.gbl.model.Page; import org.slf4j.LoggerFactory;
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData; import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails; import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.PaginationService.SortOrder;
import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentialService; import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentialService;
import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentials; import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentials;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.LmsSetupDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
final class MockupLmsAPITemplate implements LmsAPITemplate { final class MockupLmsAPITemplate implements LmsAPITemplate {
private static final Logger log = LoggerFactory.getLogger(MockupLmsAPITemplate.class);
public static final String MOCKUP_LMS_CLIENT_NAME = "mockupLmsClientName"; public static final String MOCKUP_LMS_CLIENT_NAME = "mockupLmsClientName";
public static final String MOCKUP_LMS_CLIENT_SECRET = "mockupLmsClientSecret"; public static final String MOCKUP_LMS_CLIENT_SECRET = "mockupLmsClientSecret";
private final ClientCredentialService clientCredentialService; private final ClientCredentialService clientCredentialService;
private final LmsSetupDAO lmsSetupDao; private final LmsSetup lmsSetup;
private final LmsSetup setup; private final ClientCredentials credentials;
private ClientCredentials credentials = null;
private final Collection<QuizData> mockups; private final Collection<QuizData> mockups;
MockupLmsAPITemplate( MockupLmsAPITemplate(
final LmsSetupDAO lmsSetupDao, final LmsSetup lmsSetup,
final LmsSetup setup, final ClientCredentials credentials,
final ClientCredentialService clientCredentialService) { final ClientCredentialService clientCredentialService) {
this.lmsSetupDao = lmsSetupDao; this.lmsSetup = lmsSetup;
this.clientCredentialService = clientCredentialService; this.clientCredentialService = clientCredentialService;
if (!setup.isActive() || setup.lmsType != LmsType.MOCKUP) { this.credentials = credentials;
throw new IllegalArgumentException();
}
this.setup = setup;
this.mockups = new ArrayList<>(); this.mockups = new ArrayList<>();
this.mockups.add(new QuizData( this.mockups.add(new QuizData(
"quiz1", "Demo Quiz 1", "Demo Quit Mockup", "quiz1", "Demo Quiz 1", "Demo Quit Mockup",
@ -79,88 +75,45 @@ final class MockupLmsAPITemplate implements LmsAPITemplate {
} }
@Override @Override
public Result<LmsSetup> lmsSetup() { public LmsSetup lmsSetup() {
return Result.of(this.setup); return this.lmsSetup;
} }
@Override @Override
public LmsSetupTestResult testLmsSetup() { public LmsSetupTestResult testLmsSetup() {
if (this.setup.lmsType != LmsType.MOCKUP) {
return LmsSetupTestResult.ofMissingAttributes(LMS_SETUP.ATTR_LMS_TYPE); log.info("Test Lms Binding for Mockup and LmsSetup: {}", this.lmsSetup);
final List<APIMessage> missingAttrs = attributeValidation(this.credentials);
if (!missingAttrs.isEmpty()) {
return LmsSetupTestResult.ofMissingAttributes(missingAttrs);
} }
initCredentials();
if (this.credentials != null) { if (authenticate()) {
return LmsSetupTestResult.ofOkay(); return LmsSetupTestResult.ofOkay();
} else { } else {
return LmsSetupTestResult.ofMissingAttributes( return LmsSetupTestResult.ofTokenRequestError("Illegal access");
LMS_SETUP.ATTR_LMS_URL,
LMS_SETUP.ATTR_LMS_CLIENTNAME,
LMS_SETUP.ATTR_LMS_CLIENTSECRET);
} }
} }
public Collection<QuizData> getQuizzes(
final String name,
final Long from,
final String sort) {
final int orderFactor = (SortOrder.getSortOrder(sort) == SortOrder.DESCENDING)
? -1
: 1;
final String _sort = SortOrder.decode(sort);
final Comparator<QuizData> comp = (_sort != null)
? (_sort.equals(QuizData.FILTER_ATTR_START_TIME))
? (q1, q2) -> q1.startTime.compareTo(q2.startTime) * orderFactor
: (q1, q2) -> q1.name.compareTo(q2.name) * orderFactor
: (q1, q2) -> q1.name.compareTo(q2.name) * orderFactor;
return this.mockups.stream()
.filter(mockup -> (name != null)
? mockup.name.contains(name)
: true && (from != null)
? mockup.startTime.getMillis() >= from
: true)
.sorted(comp)
.collect(Collectors.toList());
}
@Override @Override
public Result<Page<QuizData>> getQuizzes( public Result<List<QuizData>> getQuizzes(final FilterMap filterMap) {
final String name,
final Long from,
final String sort,
final int pageNumber,
final int pageSize) {
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
initCredentials(); authenticate();
if (this.credentials == null) { if (this.credentials == null) {
throw new IllegalArgumentException("Wrong clientId or secret"); throw new IllegalArgumentException("Wrong clientId or secret");
} }
final int startIndex = pageNumber * pageSize; return this.mockups.stream()
final int endIndex = startIndex + pageSize; .filter(LmsAPIService.quizzeFilterFunction(filterMap))
int index = 0; .collect(Collectors.toList());
final Collection<QuizData> quizzes = getQuizzes(name, from, sort);
final int numberOfPages = quizzes.size() / pageSize;
final Iterator<QuizData> iterator = quizzes.iterator();
final List<QuizData> pageContent = new ArrayList<>();
while (iterator.hasNext() && index < endIndex) {
final QuizData next = iterator.next();
if (index >= startIndex) {
pageContent.add(next);
}
index++;
}
return new Page<>(numberOfPages, pageNumber, sort, pageContent);
}); });
} }
@Override @Override
public Collection<Result<QuizData>> getQuizzes(final Set<String> ids) { public Collection<Result<QuizData>> getQuizzes(final Set<String> ids) {
initCredentials(); authenticate();
if (this.credentials == null) { if (this.credentials == null) {
throw new IllegalArgumentException("Wrong clientId or secret"); throw new IllegalArgumentException("Wrong clientId or secret");
} }
@ -173,7 +126,7 @@ final class MockupLmsAPITemplate implements LmsAPITemplate {
@Override @Override
public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeUserId) { public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeUserId) {
initCredentials(); authenticate();
if (this.credentials == null) { if (this.credentials == null) {
throw new IllegalArgumentException("Wrong clientId or secret"); throw new IllegalArgumentException("Wrong clientId or secret");
} }
@ -181,28 +134,23 @@ final class MockupLmsAPITemplate implements LmsAPITemplate {
return Result.of(new ExamineeAccountDetails(examineeUserId, "mockup", "mockup", "mockup")); return Result.of(new ExamineeAccountDetails(examineeUserId, "mockup", "mockup", "mockup"));
} }
@Override private boolean authenticate() {
public void reset() {
this.credentials = null;
}
private void initCredentials() {
try { try {
this.credentials = this.lmsSetupDao
.getLmsAPIAccessCredentials(this.setup.getModelId())
.getOrThrow();
final CharSequence plainClientId = this.clientCredentialService.getPlainClientId(this.credentials); final CharSequence plainClientId = this.clientCredentialService.getPlainClientId(this.credentials);
if (!"lmsMockupClientId".equals(plainClientId)) { if (!"lmsMockupClientId".equals(plainClientId)) {
throw new IllegalAccessError(); throw new IllegalAccessException("Wrong client credential");
} }
final CharSequence plainClientSecret = this.clientCredentialService.getPlainClientSecret(this.credentials); final CharSequence plainClientSecret = this.clientCredentialService.getPlainClientSecret(this.credentials);
if (!"lmsMockupSecret".equals(plainClientSecret)) { if (!"lmsMockupSecret".equals(plainClientSecret)) {
throw new IllegalAccessError(); throw new IllegalAccessException("Wrong client credential");
} }
return true;
} catch (final Exception e) { } catch (final Exception e) {
this.credentials = null; log.info("Authentication failed: ", e);
return false;
} }
} }

View file

@ -22,25 +22,37 @@ import org.slf4j.LoggerFactory;
import org.springframework.http.HttpEntity; import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.oauth2.client.OAuth2RestTemplate; import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.security.oauth2.client.resource.OAuth2AccessDeniedException;
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
import org.springframework.security.oauth2.client.resource.UserRedirectRequiredException;
import org.springframework.security.oauth2.client.token.AccessTokenRequest;
import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsAccessTokenProvider;
import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails; import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails;
import org.springframework.security.oauth2.common.OAuth2AccessToken; import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import ch.ethz.seb.sebserver.gbl.model.Domain.LMS_SETUP; import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.model.Page;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData; import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails; import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gbl.util.SupplierWithCircuitBreaker;
import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentialService; import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentialService;
import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentials; import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentials;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.LmsSetupDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
/** Implements the LmsAPITemplate for Open edX LMS Course API access.
*
* See also: https://course-catalog-api-guide.readthedocs.io */
final class OpenEdxLmsAPITemplate implements LmsAPITemplate { final class OpenEdxLmsAPITemplate implements LmsAPITemplate {
private static final Logger log = LoggerFactory.getLogger(OpenEdxLmsAPITemplate.class); private static final Logger log = LoggerFactory.getLogger(OpenEdxLmsAPITemplate.class);
@ -49,26 +61,26 @@ final class OpenEdxLmsAPITemplate implements LmsAPITemplate {
private static final String OPEN_EDX_DEFAULT_COURSE_ENDPOINT = "/api/courses/v1/courses/"; private static final String OPEN_EDX_DEFAULT_COURSE_ENDPOINT = "/api/courses/v1/courses/";
private static final String OPEN_EDX_DEFAULT_COURSE_START_URL_PREFIX = "/courses/"; private static final String OPEN_EDX_DEFAULT_COURSE_START_URL_PREFIX = "/courses/";
private final String lmsSetupId; private final LmsSetup lmsSetup;
private final LmsSetupDAO lmsSetupDAO; private final ClientCredentials credentials;
private final ClientHttpRequestFactory clientHttpRequestFactory; private final ClientHttpRequestFactory clientHttpRequestFactory;
private final ClientCredentialService clientCredentialService; private final ClientCredentialService clientCredentialService;
private final Set<String> knownTokenAccessPaths; private final Set<String> knownTokenAccessPaths;
private OAuth2RestTemplate restTemplate = null; private OAuth2RestTemplate restTemplate = null;
private SupplierWithCircuitBreaker<List<QuizData>> allQuizzesSupplier = null;
OpenEdxLmsAPITemplate( OpenEdxLmsAPITemplate(
final String lmsSetupId, final LmsSetup lmsSetup,
final LmsSetupDAO lmsSetupDAO, final ClientCredentials credentials,
final ClientCredentialService clientCredentialService, final ClientCredentialService clientCredentialService,
final ClientHttpRequestFactory clientHttpRequestFactory, final ClientHttpRequestFactory clientHttpRequestFactory,
final String[] alternativeTokenRequestPaths) { final String[] alternativeTokenRequestPaths) {
this.lmsSetupId = lmsSetupId; this.lmsSetup = lmsSetup;
this.lmsSetupDAO = lmsSetupDAO;
this.clientHttpRequestFactory = clientHttpRequestFactory;
this.clientCredentialService = clientCredentialService; this.clientCredentialService = clientCredentialService;
this.credentials = credentials;
this.clientHttpRequestFactory = clientHttpRequestFactory;
this.knownTokenAccessPaths = new HashSet<>(); this.knownTokenAccessPaths = new HashSet<>();
this.knownTokenAccessPaths.add(OPEN_EDX_DEFAULT_TOKEN_REQUEST_PATH); this.knownTokenAccessPaths.add(OPEN_EDX_DEFAULT_TOKEN_REQUEST_PATH);
if (alternativeTokenRequestPaths != null) { if (alternativeTokenRequestPaths != null) {
@ -77,87 +89,45 @@ final class OpenEdxLmsAPITemplate implements LmsAPITemplate {
} }
@Override @Override
public Result<LmsSetup> lmsSetup() { public LmsSetup lmsSetup() {
return this.lmsSetupDAO return this.lmsSetup;
.byModelId(this.lmsSetupId);
} }
@Override @Override
public LmsSetupTestResult testLmsSetup() { public LmsSetupTestResult testLmsSetup() {
final LmsSetup lmsSetup = lmsSetup().getOrThrow(); log.info("Test Lms Binding for OpenEdX and LmsSetup: {}", this.lmsSetup);
log.info("Test Lms Binding for OpenEdX and LmsSetup: {}", lmsSetup);
// validation of LmsSetup
if (lmsSetup.lmsType != LmsType.MOCKUP) {
return LmsSetupTestResult.ofMissingAttributes(LMS_SETUP.ATTR_LMS_TYPE);
}
final List<String> missingAttrs = new ArrayList<>();
if (StringUtils.isBlank(lmsSetup.lmsApiUrl)) {
missingAttrs.add(LMS_SETUP.ATTR_LMS_TYPE);
}
if (StringUtils.isBlank(lmsSetup.getLmsAuthName())) {
missingAttrs.add(LMS_SETUP.ATTR_LMS_CLIENTNAME);
}
if (StringUtils.isBlank(lmsSetup.getLmsAuthSecret())) {
missingAttrs.add(LMS_SETUP.ATTR_LMS_CLIENTSECRET);
}
final List<APIMessage> missingAttrs = attributeValidation(this.credentials);
if (!missingAttrs.isEmpty()) { if (!missingAttrs.isEmpty()) {
return LmsSetupTestResult.ofMissingAttributes(missingAttrs); return LmsSetupTestResult.ofMissingAttributes(missingAttrs);
} }
// request OAuth2 access token on OpenEdx API // request OAuth2 access token on OpenEdx API
initRestTemplateAndRequestAccessToken(lmsSetup); initRestTemplateAndRequestAccessToken();
if (this.restTemplate == null) { if (this.restTemplate == null) {
return LmsSetupTestResult.ofTokenRequestError( return LmsSetupTestResult.ofTokenRequestError(
"Failed to gain access token form OpenEdX Rest API: tried token endpoints: " + "Failed to gain access token from OpenEdX Rest API:\n tried token endpoints: " +
this.knownTokenAccessPaths); this.knownTokenAccessPaths);
} }
// query quizzes TODO!?
return LmsSetupTestResult.ofOkay(); return LmsSetupTestResult.ofOkay();
} }
@Override @Override
public Result<Page<QuizData>> getQuizzes( public Result<List<QuizData>> getQuizzes(final FilterMap filterMap) {
final String name, return this.initRestTemplateAndRequestAccessToken()
final Long from, .flatMap(this::getAllQuizes)
final String sort, .map(LmsAPIService.quizzesFilterFunction(filterMap));
final int pageNumber, }
final int pageSize) {
return this.lmsSetup() public ResponseEntity<EdXPage> getEdxPage(final String pageURI) {
.flatMap(this::initRestTemplateAndRequestAccessToken) final HttpHeaders httpHeaders = new HttpHeaders();
.map(lmsSetup -> { return this.restTemplate.exchange(
pageURI,
// TODO sort and pagination HttpMethod.GET,
final HttpHeaders httpHeaders = new HttpHeaders(); new HttpEntity<>(httpHeaders),
EdXPage.class);
final ResponseEntity<EdXPage> response = this.restTemplate.exchange(
lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_ENDPOINT,
HttpMethod.GET,
new HttpEntity<>(httpHeaders),
EdXPage.class);
final EdXPage edxpage = response.getBody();
final List<QuizData> content = edxpage.results
.stream()
.reduce(
new ArrayList<QuizData>(),
(list, courseData) -> {
list.add(quizDataOf(lmsSetup, courseData));
return list;
},
(list1, list2) -> {
list1.addAll(list2);
return list1;
});
return new Page<>(edxpage.num_pages, pageNumber, sort, content);
});
} }
@Override @Override
@ -172,20 +142,15 @@ final class OpenEdxLmsAPITemplate implements LmsAPITemplate {
return null; return null;
} }
@Override private Result<LmsSetup> initRestTemplateAndRequestAccessToken() {
public void reset() {
this.restTemplate = null;
}
private Result<LmsSetup> initRestTemplateAndRequestAccessToken(final LmsSetup lmsSetup) { log.info("Initialize Rest Template for OpenEdX API access. LmsSetup: {}", this.lmsSetup);
log.info("Initialize Rest Template for OpenEdX API access. LmsSetup: {}", lmsSetup);
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
if (this.restTemplate != null) { if (this.restTemplate != null) {
try { try {
this.restTemplate.getAccessToken(); this.restTemplate.getAccessToken();
return lmsSetup; return this.lmsSetup;
} catch (final Exception e) { } catch (final Exception e) {
log.warn( log.warn(
"Error while trying to get access token within already existing OAuth2RestTemplate instance. Try to create new one.", "Error while trying to get access token within already existing OAuth2RestTemplate instance. Try to create new one.",
@ -194,24 +159,20 @@ final class OpenEdxLmsAPITemplate implements LmsAPITemplate {
} }
} }
final ClientCredentials credentials = this.lmsSetupDAO
.getLmsAPIAccessCredentials(this.lmsSetupId)
.getOrThrow();
final Iterator<String> tokenAccessPaths = this.knownTokenAccessPaths.iterator(); final Iterator<String> tokenAccessPaths = this.knownTokenAccessPaths.iterator();
while (tokenAccessPaths.hasNext()) { while (tokenAccessPaths.hasNext()) {
final String accessTokenRequestPath = tokenAccessPaths.next(); final String accessTokenRequestPath = tokenAccessPaths.next();
try { try {
final OAuth2RestTemplate template = createRestTemplate( final OAuth2RestTemplate template = createRestTemplate(
lmsSetup, this.lmsSetup,
credentials, this.credentials,
accessTokenRequestPath); accessTokenRequestPath);
final OAuth2AccessToken accessToken = template.getAccessToken(); final OAuth2AccessToken accessToken = template.getAccessToken();
if (accessToken != null) { if (accessToken != null) {
this.restTemplate = template; this.restTemplate = template;
return lmsSetup; return this.lmsSetup;
} }
} catch (final Exception e) { } catch (final Exception e) {
log.info("Failed to request access token on access token request path: {}", accessTokenRequestPath, log.info("Failed to request access token on access token request path: {}", accessTokenRequestPath,
@ -219,7 +180,8 @@ final class OpenEdxLmsAPITemplate implements LmsAPITemplate {
} }
} }
throw new IllegalArgumentException("Unable to establish OpenEdX API connection for lmsSetup: " + lmsSetup); throw new IllegalArgumentException(
"Unable to establish OpenEdX API connection for lmsSetup: " + this.lmsSetup);
}); });
} }
@ -235,17 +197,49 @@ final class OpenEdxLmsAPITemplate implements LmsAPITemplate {
details.setAccessTokenUri(lmsSetup.lmsApiUrl + accessTokenRequestPath); details.setAccessTokenUri(lmsSetup.lmsApiUrl + accessTokenRequestPath);
details.setClientId(plainClientId.toString()); details.setClientId(plainClientId.toString());
details.setClientSecret(plainClientSecret.toString()); details.setClientSecret(plainClientSecret.toString());
details.setGrantType("client_credentials");
// TODO: accordingly to the documentation (https://course-catalog-api-guide.readthedocs.io/en/latest/authentication/#create-an-account-on-edx-org-for-api-access)
// token_type=jwt is needed for token request but is it possible to set this within ClientCredentialsResourceDetails
// or within the request header on API call. To clarify
final OAuth2RestTemplate template = new OAuth2RestTemplate(details); final OAuth2RestTemplate template = new OAuth2RestTemplate(details);
template.setRequestFactory(this.clientHttpRequestFactory); template.setRequestFactory(this.clientHttpRequestFactory);
template.setAccessTokenProvider(new EdxClientCredentialsAccessTokenProvider());
return template; return template;
} }
private Result<List<QuizData>> getAllQuizes(final LmsSetup lmsSetup) {
if (this.allQuizzesSupplier == null) {
this.allQuizzesSupplier = new SupplierWithCircuitBreaker<>(
() -> collectAllCourses(lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_ENDPOINT)
.stream()
.reduce(
new ArrayList<QuizData>(),
(list, courseData) -> {
list.add(quizDataOf(lmsSetup, courseData));
return list;
},
(list1, list2) -> {
list1.addAll(list2);
return list1;
}),
5, 1000L); // TODO specify better CircuitBreaker params
}
return this.allQuizzesSupplier.get();
}
private List<CourseData> collectAllCourses(final String pageURI) {
final List<CourseData> collector = new ArrayList<>();
EdXPage page = getEdxPage(pageURI).getBody();
if (page != null) {
collector.addAll(page.results);
while (StringUtils.isNoneBlank(page.next)) {
page = getEdxPage(page.next).getBody();
collector.addAll(page.results);
}
}
return collector;
}
private QuizData quizDataOf( private QuizData quizDataOf(
final LmsSetup lmsSetup, final LmsSetup lmsSetup,
final CourseData courseData) { final CourseData courseData) {
@ -260,14 +254,16 @@ final class OpenEdxLmsAPITemplate implements LmsAPITemplate {
startURI); startURI);
} }
/** Maps a OpenEdX course API course page */
static final class EdXPage { static final class EdXPage {
public Integer count; public Integer count;
public Integer previous; public String previous;
public Integer num_pages; public Integer num_pages;
public Integer next; public String next;
public List<CourseData> results; public List<CourseData> results;
} }
/** Maps the OpenEdX course API course data */
static final class CourseData { static final class CourseData {
public String id; public String id;
public String course_id; public String course_id;
@ -278,41 +274,31 @@ final class OpenEdxLmsAPITemplate implements LmsAPITemplate {
public String end; public String end;
} }
/* /** A custom ClientCredentialsAccessTokenProvider that adapts the access token request to Open edX
* pagination * access token request protocol using a form-URL-encoded POST request according to:
* count 2 * https://course-catalog-api-guide.readthedocs.io/en/latest/authentication/index.html#getting-an-access-token */
* previous null private class EdxClientCredentialsAccessTokenProvider extends ClientCredentialsAccessTokenProvider {
* num_pages 1
* next null @Override
* results public OAuth2AccessToken obtainAccessToken(
* 0 final OAuth2ProtectedResourceDetails details,
* blocks_url "http://ralph.ethz.ch:18000/api/courses/v1/blocks/?course_id=course-v1%3AedX%2BDemoX%2BDemo_Course" final AccessTokenRequest request)
* effort null throws UserRedirectRequiredException,
* end null AccessDeniedException,
* enrollment_start null OAuth2AccessDeniedException {
* enrollment_end null
* id "course-v1:edX+DemoX+Demo_Course" final ClientCredentialsResourceDetails resource = (ClientCredentialsResourceDetails) details;
* media final HttpHeaders headers = new HttpHeaders();
* course_image headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE);
* uri "/asset-v1:edX+DemoX+Demo_Course+type@asset+block@images_course_image.jpg"
* course_video final MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
* uri null params.add("grant_type", "client_credentials");
* image params.add("token_type", "jwt");
* raw "http://ralph.ethz.ch:18000/asset-v1:edX+DemoX+Demo_Course+type@asset+block@images_course_image.jpg" params.add("client_id", resource.getClientId());
* small "http://ralph.ethz.ch:18000/asset-v1:edX+DemoX+Demo_Course+type@asset+block@images_course_image.jpg" params.add("client_secret", resource.getClientSecret());
* large "http://ralph.ethz.ch:18000/asset-v1:edX+DemoX+Demo_Course+type@asset+block@images_course_image.jpg"
* name "edX Demonstration Course" return retrieveToken(request, resource, params, headers);
* number "DemoX" }
* org "edX" }
* short_description null
* start "2013-02-05T05:00:00Z"
* start_display "Feb. 5, 2013"
* start_type "timestamp"
* pacing "instructor"
* mobile_available false
* hidden false
* invitation_only false
* course_id "course-v1:edX+DemoX+Demo_Course"
*/
} }

View file

@ -137,7 +137,7 @@ public class APIExceptionHandler extends ResponseEntityExceptionHandler {
final WebRequest request) { final WebRequest request) {
return new ResponseEntity<>( return new ResponseEntity<>(
Arrays.asList(ex.getAPIMessage()), ex.getAPIMessages(),
HttpStatus.BAD_REQUEST); HttpStatus.BAD_REQUEST);
} }

View file

@ -253,7 +253,7 @@ public class ExamAdministrationController extends ActivatableEntityController<Ex
final String quizId = postParams.getString(QuizData.QUIZ_ATTR_ID); final String quizId = postParams.getString(QuizData.QUIZ_ATTR_ID);
final LmsAPITemplate lmsAPITemplate = this.lmsAPIService final LmsAPITemplate lmsAPITemplate = this.lmsAPIService
.createLmsAPITemplate(lmsSetupId) .getLmsAPITemplate(lmsSetupId)
.getOrThrow(); .getOrThrow();
final QuizData quiz = lmsAPITemplate.getQuizzes(new HashSet<>(Arrays.asList(quizId))) final QuizData quiz = lmsAPITemplate.getQuizzes(new HashSet<>(Arrays.asList(quizId)))

View file

@ -16,6 +16,7 @@ import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import ch.ethz.seb.sebserver.gbl.api.API; import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.APIMessage.APIMessageException;
import ch.ethz.seb.sebserver.gbl.api.EntityType; import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.api.POSTMapper; import ch.ethz.seb.sebserver.gbl.api.POSTMapper;
import ch.ethz.seb.sebserver.gbl.authorization.PrivilegeType; import ch.ethz.seb.sebserver.gbl.authorization.PrivilegeType;
@ -75,9 +76,15 @@ public class LmsSetupController extends ActivatableEntityController<LmsSetup, Lm
this.authorization.check(PrivilegeType.MODIFY, EntityType.LMS_SETUP); this.authorization.check(PrivilegeType.MODIFY, EntityType.LMS_SETUP);
return this.lmsAPIService.createLmsAPITemplate(modelId) final LmsSetupTestResult result = this.lmsAPIService.getLmsAPITemplate(modelId)
.map(template -> template.testLmsSetup()) .map(template -> template.testLmsSetup())
.getOrThrow(); .getOrThrow();
if (result.missingLMSSetupAttribute != null && !result.missingLMSSetupAttribute.isEmpty()) {
throw new APIMessageException(result.missingLMSSetupAttribute);
}
return result;
} }
@Override @Override

View file

@ -9,6 +9,7 @@
package ch.ethz.seb.sebserver.webservice.weblayer.api; package ch.ethz.seb.sebserver.webservice.weblayer.api;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
@ -17,16 +18,14 @@ import org.springframework.web.bind.annotation.RestController;
import ch.ethz.seb.sebserver.gbl.api.API; import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.EntityType; import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.authorization.PrivilegeType; import ch.ethz.seb.sebserver.gbl.authorization.PrivilegeType;
import ch.ethz.seb.sebserver.gbl.model.Domain.LMS_SETUP;
import ch.ethz.seb.sebserver.gbl.model.Entity; import ch.ethz.seb.sebserver.gbl.model.Entity;
import ch.ethz.seb.sebserver.gbl.model.Page; import ch.ethz.seb.sebserver.gbl.model.Page;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData; import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.AuthorizationService; import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.AuthorizationService;
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.UserService; import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.UserService;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
@WebServiceProfile @WebServiceProfile
@RestController @RestController
@ -57,26 +56,20 @@ public class QuizImportController {
name = Entity.FILTER_ATTR_INSTITUTION, name = Entity.FILTER_ATTR_INSTITUTION,
required = true, required = true,
defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId, defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId,
@RequestParam(name = LMS_SETUP.ATTR_ID, required = true) final Long lmsSetupId,
@RequestParam(name = QuizData.FILTER_ATTR_NAME, required = false) final String nameLike,
@RequestParam(name = QuizData.FILTER_ATTR_START_TIME, required = false) final String startTime,
@RequestParam(name = Page.ATTR_PAGE_NUMBER, required = false) final Integer pageNumber, @RequestParam(name = Page.ATTR_PAGE_NUMBER, required = false) final Integer pageNumber,
@RequestParam(name = Page.ATTR_PAGE_SIZE, required = false) final Integer pageSize, @RequestParam(name = Page.ATTR_PAGE_SIZE, required = false) final Integer pageSize,
@RequestParam(name = Page.ATTR_SORT, required = false) final String sort) { @RequestParam(name = Page.ATTR_SORT, required = false) final String sort,
@RequestParam final MultiValueMap<String, String> allRequestParams) {
final LmsAPITemplate lmsAPITemplate = this.lmsAPIService
.createLmsAPITemplate(lmsSetupId)
.getOrThrow();
this.authorization.check( this.authorization.check(
PrivilegeType.READ_ONLY, PrivilegeType.READ_ONLY,
EntityType.EXAM, EntityType.EXAM,
institutionId); institutionId);
return lmsAPITemplate.getQuizzes( final FilterMap filterMap = new FilterMap(allRequestParams);
nameLike, filterMap.putIfAbsent(Entity.FILTER_ATTR_INSTITUTION, String.valueOf(institutionId));
Utils.dateTimeStringToTimestamp(startTime, null),
sort, return this.lmsAPIService.requestQuizDataPage(
(pageNumber != null) (pageNumber != null)
? pageNumber ? pageNumber
: 1, : 1,
@ -84,7 +77,9 @@ public class QuizImportController {
? (pageSize <= this.maxPageSize) ? (pageSize <= this.maxPageSize)
? pageSize ? pageSize
: this.maxPageSize : this.maxPageSize
: this.defaultPageSize) : this.defaultPageSize,
sort,
filterMap)
.getOrThrow(); .getOrThrow();
} }

View file

@ -26,6 +26,7 @@ sebserver.form.validation.fieldError.notNull=This field is mandatory
sebserver.form.validation.fieldError.username.notunique=This Username is already in use. Please choose another one. sebserver.form.validation.fieldError.username.notunique=This Username is already in use. Please choose another one.
sebserver.form.validation.fieldError.password.wrong=Old password is wrong sebserver.form.validation.fieldError.password.wrong=Old password is wrong
sebserver.form.validation.fieldError.password.mismatch=Re-typed password don't match new password sebserver.form.validation.fieldError.password.mismatch=Re-typed password don't match new password
sebserver.form.validation.fieldError.invalidURL=The input does not match the URL pattern.
sebserver.error.unexpected=Unexpected Error sebserver.error.unexpected=Unexpected Error
sebserver.page.message=Information sebserver.page.message=Information
sebserver.dialog.confirm.title=Confirmation sebserver.dialog.confirm.title=Confirmation
@ -159,6 +160,12 @@ sebserver.lmssetup.action.new=New LMS Setup
sebserver.lmssetup.action.list.view=View Selected sebserver.lmssetup.action.list.view=View Selected
sebserver.lmssetup.action.list.modify=Edit Selected sebserver.lmssetup.action.list.modify=Edit Selected
sebserver.lmssetup.action.modify=Edit sebserver.lmssetup.action.modify=Edit
sebserver.lmssetup.action.test=Test Setup
sebserver.lmssetup.action.test.ok=Successfully connect to the LMSs course API
sebserver.lmssetup.action.test.tokenRequestError=The API access was denied: {0}
sebserver.lmssetup.action.test.quizRequestError=Unable to request courses or quizzes from the course API of the LMS. {0}
sebserver.lmssetup.action.test.missingParameter=There is one or more missing connection parameter.<br/>Please check the connection parameter for this LMS Setup
sebserver.lmssetup.action.test.unknownError=An unexpected error happened while trying to connect to the LMS course API. {0}
sebserver.lmssetup.action.save=Save LMS Setup sebserver.lmssetup.action.save=Save LMS Setup
sebserver.lmssetup.action.activate=Active sebserver.lmssetup.action.activate=Active
sebserver.lmssetup.action.deactivate=Active sebserver.lmssetup.action.deactivate=Active

View file

@ -3,10 +3,13 @@ SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0;
SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0; SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0;
SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='TRADITIONAL,ALLOW_INVALID_DATES'; SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='TRADITIONAL,ALLOW_INVALID_DATES';
-- ----------------------------------------------------- -- -----------------------------------------------------
-- Schema SEBServer -- Schema SEBServer
-- ----------------------------------------------------- -- -----------------------------------------------------
-- ----------------------------------------------------- -- -----------------------------------------------------
-- Table `institution` -- Table `institution`
-- ----------------------------------------------------- -- -----------------------------------------------------

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 B

View file

@ -32,23 +32,23 @@ public class ClientCredentialServiceTest {
@Test @Test
public void testEncryptDecryptClientCredentials() { public void testEncryptDecryptClientCredentials() {
final Environment envMock = mock(Environment.class); final Environment envMock = mock(Environment.class);
when(envMock.getRequiredProperty(ClientCredentialService.SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY)) when(envMock.getRequiredProperty(ClientCredentialServiceImpl.SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY))
.thenReturn("secret1"); .thenReturn("secret1");
final String clientName = "simpleClientName"; final String clientName = "simpleClientName";
final ClientCredentialService service = new ClientCredentialService(envMock); final ClientCredentialServiceImpl service = new ClientCredentialServiceImpl(envMock);
String encrypted = String encrypted =
service.encrypt(clientName, "secret1", ClientCredentialService.DEFAULT_SALT).toString(); service.encrypt(clientName, "secret1", ClientCredentialServiceImpl.DEFAULT_SALT).toString();
String decrypted = service.decrypt(encrypted, "secret1", ClientCredentialService.DEFAULT_SALT).toString(); String decrypted = service.decrypt(encrypted, "secret1", ClientCredentialServiceImpl.DEFAULT_SALT).toString();
assertEquals(clientName, decrypted); assertEquals(clientName, decrypted);
final String clientSecret = "fbjreij39ru29305ruࣣàèLöäöäü65%(/%(ç87"; final String clientSecret = "fbjreij39ru29305ruࣣàèLöäöäü65%(/%(ç87";
encrypted = encrypted =
service.encrypt(clientSecret, "secret1", ClientCredentialService.DEFAULT_SALT).toString(); service.encrypt(clientSecret, "secret1", ClientCredentialServiceImpl.DEFAULT_SALT).toString();
decrypted = service.decrypt(encrypted, "secret1", ClientCredentialService.DEFAULT_SALT).toString(); decrypted = service.decrypt(encrypted, "secret1", ClientCredentialServiceImpl.DEFAULT_SALT).toString();
assertEquals(clientSecret, decrypted); assertEquals(clientSecret, decrypted);
} }