refactoring and improvement of LMS binding API

This commit is contained in:
anhefti 2021-05-14 17:08:29 +02:00
parent 0bda2cb292
commit dd6150ec2a
36 changed files with 790 additions and 544 deletions

View file

@ -129,6 +129,7 @@ public final class API {
public static final String EXAM_ADMINISTRATION_ENDPOINT = "/exam"; public static final String EXAM_ADMINISTRATION_ENDPOINT = "/exam";
public static final String EXAM_ADMINISTRATION_DOWNLOAD_CONFIG_PATH_SEGMENT = "/download-config"; public static final String EXAM_ADMINISTRATION_DOWNLOAD_CONFIG_PATH_SEGMENT = "/download-config";
public static final String EXAM_ADMINISTRATION_CONSISTENCY_CHECK_PATH_SEGMENT = "/check-consistency"; public static final String EXAM_ADMINISTRATION_CONSISTENCY_CHECK_PATH_SEGMENT = "/check-consistency";
public static final String EXAM_ADMINISTRATION_CONSISTENCY_CHECK_INCLUDE_RESTRICTION = "include-restriction";
public static final String EXAM_ADMINISTRATION_SEB_RESTRICTION_PATH_SEGMENT = "/seb-restriction"; public static final String EXAM_ADMINISTRATION_SEB_RESTRICTION_PATH_SEGMENT = "/seb-restriction";
public static final String EXAM_ADMINISTRATION_CHECK_RESTRICTION_PATH_SEGMENT = "/check-seb-restriction"; public static final String EXAM_ADMINISTRATION_CHECK_RESTRICTION_PATH_SEGMENT = "/check-seb-restriction";
public static final String EXAM_ADMINISTRATION_CHECK_IMPORTED_PATH_SEGMENT = "/check-imported"; public static final String EXAM_ADMINISTRATION_CHECK_IMPORTED_PATH_SEGMENT = "/check-imported";

View file

@ -47,7 +47,7 @@ public final class Exam implements GrantEntity {
null, null,
null, null,
ExamStatus.FINISHED, ExamStatus.FINISHED,
// Boolean.FALSE, Boolean.FALSE,
null, null,
Boolean.FALSE, Boolean.FALSE,
null); null);
@ -115,6 +115,9 @@ public final class Exam implements GrantEntity {
@JsonProperty(EXAM.ATTR_STATUS) @JsonProperty(EXAM.ATTR_STATUS)
public final ExamStatus status; public final ExamStatus status;
@JsonProperty(EXAM.ATTR_LMS_SEB_RESTRICTION)
public final Boolean sebRestriction;
@JsonProperty(EXAM.ATTR_BROWSER_KEYS) @JsonProperty(EXAM.ATTR_BROWSER_KEYS)
public final String browserExamKeys; public final String browserExamKeys;
@ -139,6 +142,7 @@ public final class Exam implements GrantEntity {
@JsonProperty(EXAM.ATTR_OWNER) final String owner, @JsonProperty(EXAM.ATTR_OWNER) final String owner,
@JsonProperty(EXAM.ATTR_SUPPORTER) final Collection<String> supporter, @JsonProperty(EXAM.ATTR_SUPPORTER) final Collection<String> supporter,
@JsonProperty(EXAM.ATTR_STATUS) final ExamStatus status, @JsonProperty(EXAM.ATTR_STATUS) final ExamStatus status,
@JsonProperty(EXAM.ATTR_LMS_SEB_RESTRICTION) final Boolean sebRestriction,
@JsonProperty(EXAM.ATTR_BROWSER_KEYS) final String browserExamKeys, @JsonProperty(EXAM.ATTR_BROWSER_KEYS) final String browserExamKeys,
@JsonProperty(EXAM.ATTR_ACTIVE) final Boolean active, @JsonProperty(EXAM.ATTR_ACTIVE) final Boolean active,
@JsonProperty(EXAM.ATTR_LASTUPDATE) final String lastUpdate) { @JsonProperty(EXAM.ATTR_LASTUPDATE) final String lastUpdate) {
@ -155,6 +159,7 @@ public final class Exam implements GrantEntity {
this.type = type; this.type = type;
this.owner = owner; this.owner = owner;
this.status = (status != null) ? status : getStatusFromDate(startTime, endTime); this.status = (status != null) ? status : getStatusFromDate(startTime, endTime);
this.sebRestriction = sebRestriction;
this.browserExamKeys = browserExamKeys; this.browserExamKeys = browserExamKeys;
this.active = (active != null) ? active : Boolean.TRUE; this.active = (active != null) ? active : Boolean.TRUE;
this.lastUpdate = lastUpdate; this.lastUpdate = lastUpdate;
@ -181,6 +186,7 @@ public final class Exam implements GrantEntity {
EXAM.ATTR_STATUS, EXAM.ATTR_STATUS,
ExamStatus.class, ExamStatus.class,
getStatusFromDate(this.startTime, this.endTime)); getStatusFromDate(this.startTime, this.endTime));
this.sebRestriction = null;
this.browserExamKeys = mapper.getString(EXAM.ATTR_BROWSER_KEYS); this.browserExamKeys = mapper.getString(EXAM.ATTR_BROWSER_KEYS);
this.active = mapper.getBoolean(EXAM.ATTR_ACTIVE); this.active = mapper.getBoolean(EXAM.ATTR_ACTIVE);
this.supporter = mapper.getStringSet(EXAM.ATTR_SUPPORTER); this.supporter = mapper.getStringSet(EXAM.ATTR_SUPPORTER);
@ -204,6 +210,7 @@ public final class Exam implements GrantEntity {
this.type = null; this.type = null;
this.owner = null; this.owner = null;
this.status = (status != null) ? status : getStatusFromDate(this.startTime, this.endTime); this.status = (status != null) ? status : getStatusFromDate(this.startTime, this.endTime);
this.sebRestriction = null;
this.browserExamKeys = null; this.browserExamKeys = null;
this.active = null; this.active = null;
this.supporter = null; this.supporter = null;

View file

@ -29,6 +29,7 @@ import ch.ethz.seb.sebserver.gbl.util.Utils;
public final class QuizData implements GrantEntity { public final class QuizData implements GrantEntity {
public static final String FILTER_ATTR_QUIZ_NAME = "quiz_name";
public static final String FILTER_ATTR_START_TIME = "start_timestamp"; public static final String FILTER_ATTR_START_TIME = "start_timestamp";
public static final String ATTR_ADDITIONAL_ATTRIBUTES = "ADDITIONAL_ATTRIBUTES"; public static final String ATTR_ADDITIONAL_ATTRIBUTES = "ADDITIONAL_ATTRIBUTES";

View file

@ -37,16 +37,32 @@ public final class LmsSetup implements GrantEntity, Activatable {
public static final String FILTER_ATTR_LMS_SETUP = "lms_setup"; 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";
/** LMS binding and API features */
public enum Features { public enum Features {
/** The course API allows the application to securely connect to a LMS service
* and request course or quiz data from that LMS as well as requesting some
* limited LMS user account data like user name or display name. */
COURSE_API, COURSE_API,
/** The SEB restriction API allows the application to securely connect to a LMS service
* and place or release SEB restrictions, for a particular course or quiz, on the LMS.
* The SEB restriciton is usually in the form of certain hash keys and addition
* restriction settings that prompt the LMS to check access on course/quiz connection and
* allow only access for a dedicated SEB client with the right configuration in place. */
SEB_RESTRICTION SEB_RESTRICTION
} }
/** Defines the supported types if LMS bindings.
* Also defines the supports feature(s) for each type of LMS binding. */
public enum LmsType { public enum LmsType {
/** Mockup LMS type used to create test setups */
MOCKUP(Features.COURSE_API), MOCKUP(Features.COURSE_API),
/** The Open edX LMS binding features both APIs, course access as well as SEB restrcition */
OPEN_EDX(Features.COURSE_API, Features.SEB_RESTRICTION), OPEN_EDX(Features.COURSE_API, Features.SEB_RESTRICTION),
/** The Moodle binding features only the course access API so far */
MOODLE(Features.COURSE_API /* , Features.SEB_RESTRICTION */), MOODLE(Features.COURSE_API /* , Features.SEB_RESTRICTION */),
/** The Ans Delft binding is on the way */
ANS_DELFT(/* Features.COURSE_API , Features.SEB_RESTRICTION */), ANS_DELFT(/* Features.COURSE_API , Features.SEB_RESTRICTION */),
/** The OpenOLAT binding is on the way */
OPEN_OLAT(/* Features.COURSE_API , Features.SEB_RESTRICTION */); OPEN_OLAT(/* Features.COURSE_API , Features.SEB_RESTRICTION */);
public final EnumSet<Features> features; public final EnumSet<Features> features;

View file

@ -226,6 +226,8 @@ public class ExamForm implements TemplateComposer {
final ExamStatus examStatus = exam.getStatus(); final ExamStatus examStatus = exam.getStatus();
final boolean editable = modifyGrant && (examStatus == ExamStatus.UP_COMING || final boolean editable = modifyGrant && (examStatus == ExamStatus.UP_COMING ||
examStatus == ExamStatus.RUNNING); examStatus == ExamStatus.RUNNING);
// TODO this is not performat try to improve by doing one check with the CheckExamConsistency above
final boolean sebRestrictionAvailable = testSEBRestrictionAPI(exam); final boolean sebRestrictionAvailable = testSEBRestrictionAPI(exam);
final boolean isRestricted = readonly && sebRestrictionAvailable && this.restService final boolean isRestricted = readonly && sebRestrictionAvailable && this.restService
.getBuilder(CheckSEBRestriction.class) .getBuilder(CheckSEBRestriction.class)

View file

@ -29,4 +29,5 @@ public interface LmsSetupDAO extends ActivatableEntityDAO<LmsSetup, LmsSetup>, B
* @param lmsSetupId the LMS Setup identifier * @param lmsSetupId the LMS Setup identifier
* @return Result refer to the proxy data or to an error if happened */ * @return Result refer to the proxy data or to an error if happened */
Result<ProxyData> getLmsAPIAccessProxyData(String lmsSetupId); Result<ProxyData> getLmsAPIAccessProxyData(String lmsSetupId);
} }

View file

@ -1010,6 +1010,7 @@ public class ExamDAOImpl implements ExamDAO {
record.getOwner(), record.getOwner(),
supporter, supporter,
(quizData != null) ? status : (statusOverride != null) ? statusOverride : status, (quizData != null) ? status : (statusOverride != null) ? statusOverride : status,
BooleanUtils.toBooleanObject(record.getLmsSebRestriction()),
record.getBrowserKeys(), record.getBrowserKeys(),
BooleanUtils.toBooleanObject((quizData != null) ? record.getActive() : null), BooleanUtils.toBooleanObject((quizData != null) ? record.getActive() : null),
record.getLastupdate()); record.getLastupdate());

View file

@ -41,17 +41,6 @@ public interface ExamAdminService {
* @return Result refer to the restriction flag or to an error when happened */ * @return Result refer to the restriction flag or to an error when happened */
Result<Boolean> isRestricted(Exam exam); Result<Boolean> isRestricted(Exam exam);
// /** Get the proctoring service settings for a certain exam to an error when happened.
// *
// * @param examId the exam instance
// * @return Result refer to proctoring service settings for the exam. */
// default Result<ProctoringServiceSettings> getProctoringServiceSettings(final Exam exam) {
// if (exam == null || exam.id == null) {
// return Result.ofRuntimeError("Invalid Exam model");
// }
// return getProctoringServiceSettings(exam.id);
// }
/** Get proctoring service settings for a certain exam to an error when happened. /** Get proctoring service settings for a certain exam to an error when happened.
* *
* @param examId the exam identifier * @param examId the exam identifier
@ -99,29 +88,4 @@ public interface ExamAdminService {
.flatMap(settings -> getExamProctoringService(settings.serverType)); .flatMap(settings -> getExamProctoringService(settings.serverType));
} }
// /** Get the exam proctoring service implementation of specified type.
// *
// * @param settings the ProctoringSettings that defines the ProctoringServerType
// * @return ExamProctoringService instance */
// default Result<ExamProctoringService> getExamProctoringService(final ProctoringServiceSettings settings) {
// return Result.tryCatch(() -> getExamProctoringService(settings.serverType).getOrThrow());
// }
//
// /** Get the exam proctoring service implementation for specified exam.
// *
// * @param exam the exam instance
// * @return ExamProctoringService instance */
// default Result<ExamProctoringService> getExamProctoringService(final Exam exam) {
// return Result.tryCatch(() -> getExamProctoringService(exam.id).getOrThrow());
// }
//
// /** Get the exam proctoring service implementation for specified exam.
// *
// * @param examId the exam identifier
// * @return ExamProctoringService instance */
// default Result<ExamProctoringService> getExamProctoringService(final Long examId) {
// return getProctoringServiceSettings(examId)
// .flatMap(this::getExamProctoringService);
// }
} }

View file

@ -0,0 +1,33 @@
/*
* Copyright (c) 2021 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;
import ch.ethz.seb.sebserver.gbl.client.ClientCredentials;
import ch.ethz.seb.sebserver.gbl.client.ProxyData;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
/** Supplier for LmsAPITemplate to supply the templat with the needed LMS connection data. */
public interface APITemplateDataSupplier {
/** Get the LmsSetup instance containing all setup attributes
*
* @return the LmsSetup instance containing all setup attributes */
LmsSetup getLmsSetup();
/** Get the encoded LMS setup client credentials needed to access the LMS API.
*
* @return the encoded LMS setup client credentials needed to access the LMS API. */
ClientCredentials getLmsClientCredentials();
/** Get the proxy data if available and if needed for LMS connection
*
* @return the proxy data if available and if needed for LMS connection */
ProxyData getProxyData();
}

View file

@ -35,6 +35,9 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
* changes this service will be notifies about the change and release the related LmsAPITemplate from cache. */ * changes this service will be notifies about the change and release the related LmsAPITemplate from cache. */
public interface LmsAPIService { public interface LmsAPIService {
/** Reset and cleanup the caches if there are some */
void cleanup();
/** Get the specified LmsSetup model by primary key /** Get the specified LmsSetup model by primary key
* *
* @param id The identifier (PK) of the LmsSetup model * @param id The identifier (PK) of the LmsSetup model

View file

@ -17,6 +17,7 @@ import java.util.Set;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import ch.ethz.seb.sebserver.gbl.api.EntityType; import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.async.MemoizingCircuitBreaker;
import ch.ethz.seb.sebserver.gbl.model.exam.Chapters; import ch.ethz.seb.sebserver.gbl.model.exam.Chapters;
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.QuizData; import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
@ -27,64 +28,114 @@ 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.dao.FilterMap; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ResourceNotFoundException; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ResourceNotFoundException;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.AbstractCourseAccess;
/** Defines an LMS API access template to build SEB Server LMS integration. /** Defines an LMS API access template to build SEB Server LMS integration.
* * </p>
* A LMS integration consists of two main parts so far: * A LMS integration consists of two main parts so far:
* - The course API to search and request course data from LMS as well as resolve some LMS account details for a given * </p>
* examineeId
* - The SEB restriction API to apply SEB restriction data to the LMS to restrict a certain course for SEB
* *
* A LmsAPITemplate is been constructed within a LmsSetup that defines the LMS setup data that is needed to connect to * <pre>
* a specific LMS instance of implemented type. * - The course API to search and request course data from LMS as well as resolve some
* * LMS account details for a given examineeId.
* The enum LmsSetup.LmsType defines the supported LMS types and for each type the supported API part(s). * - The SEB restriction API to apply SEB restriction data to the LMS to restrict a
* * certain course for SEB.
* SEB Server uses the test functions that are defined for each LMS API part to test API access for a certain LMS * </pre>
* instance respectively the underling LMSSetup. Concrete implementations can do various tests to check full * </p>
* or partial API Access and can flag missing or wrong LMSSetup attributes with the resulting LmsSetupTestResult.
* *
* <b>Course API</b></br>
* All course API requests of this template shall not block and return as fast as possible
* with the best result it can provide for the time on that the request was made.
* </p>
* Since the course API requests course data from potentially thousands of existing and
* active courses, the course API can implement some caches if needed.</br>
* A cache in the course API has manly two purposes; The first and prior purpose is to
* be able to provide course data as fast as possible even if the LMS is not available or
* busy at the time. The second purpose is to guarantee fast data access for the system
* if this is needed and data actuality has second priority.</br>
* Therefore usual get quiz data functions like {@link #getQuizzes(FilterMap filterMap) },
* {@link #getQuizzes(Set<String> ids) } and {@link #getQuiz(final String id) }
* shall always first try to connect to the LMS and request the specified data from the LMS.
* If this succeeds the cache shall be updated with the received quizzes data and return them.
* If this is not possible within a certain time, the implementation shall get as much of the
* requested data from the cache and return them to the caller to not block the call too long
* and allow the caller to return fast and present as much data as possible.</br>
* This can be done with a {@link MemoizingCircuitBreaker} or a simple {@link CircuitBreaker}
* with a separated cache, for example. The abstract implementation; {@link AbstractCourseAccess}
* provides already defined wrapped circuit breaker for each call. To use it, just extend the
* abstract class and implement the needed suppliers.</br>
* On the other hand, dedicated cache access functions like {@link #getQuizzesFromCache(Set<String> ids) }
* shall always first look into the cache to geht the requested data and if not
* available, call the LMS and request the data from the LMS. If partial data is needed to get
* be requested from the LMS, this functions shall also update the catch with the requested
* and cache missed data afterwards.
* </p>
* <b>SEB restriction API</b></br>
* For this API we need no caching since this is mostly about pushing data to the LMS for the LMS
* to use. But this calls sahl also be protected within some kind of circuit breaker pattern to
* avoid blocking on long latency.
* </p>
* </p>
* A {@link LmsAPITemplate } will be constructed within the application with a {@link LmsSetup } instances.
* The application constructs a {@link LmsAPITemplate } for each type of LMS setup when needed or requested and
* there is not already a cached template or the cached template is out of date.</br>
* The {@link LmsSetup } defines the data that is needed to connect to a specific LMS instance of implemented type
* and is wrapped within a {@link LmsAPITemplate } instance that lives as long as there are no changes to the
* {@link LmsSetup and the {@link LmsSetup } that is wrapped within the {@link LmsAPITemplate } is up to date.
* <p>
* The enum {@link LmsSetup.LmsType } defines the supported LMS types and for each type the supported API part(s).
* <p>
* The application uses the test functions that are defined for each LMS API part to test API access for a certain LMS
* instance respectively the underling {@link LmsSetup }. Concrete implementations can do various tests to check full
* or partial API Access and can flag missing or wrong {@link LmsSetup } attributes with the resulting
* {@link LmsSetupTestResult }.</br>
* SEB Server than uses an instance of this template to communicate with the an LMS. */ * SEB Server than uses an instance of this template to communicate with the an LMS. */
public interface LmsAPITemplate { public interface LmsAPITemplate {
/** Get the underling LMSSetup configuration for this LmsAPITemplate /** Get the LMS type of the concrete template implementation
* *
* @return the underling LMSSetup configuration for this LmsAPITemplate */ * @return the LMS type of the concrete template implementation */
LmsSetup.LmsType getType();
/** Get the underling {@link LmsSetup } configuration for this LmsAPITemplate
*
* @return the underling {@link LmsSetup } configuration for this LmsAPITemplate */
LmsSetup lmsSetup(); LmsSetup lmsSetup();
/** Performs a test for the underling LmsSetup configuration and checks if the // *******************************************************************
// **** Course API functions *****************************************
/** Performs a test for the underling {@link LmsSetup } configuration and checks if the
* LMS and the course API of the LMS can be accessed or if there are some difficulties, * LMS and the course API of the LMS can be accessed or if there are some difficulties,
* missing configuration data or connection/authentication errors. * missing configuration data or connection/authentication errors.
* *
* @return LmsSetupTestResult instance with the test result report */ * @return {@link LmsSetupTestResult } instance with the test result report */
LmsSetupTestResult testCourseAccessAPI(); LmsSetupTestResult testCourseAccessAPI();
/** Performs a test for the underling LmsSetup configuration and checks if the /** Get an unsorted List of filtered {@link QuizData } from the LMS course/quiz API
* LMS and the course restriction 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 */ * @param filterMap the {@link FilterMap } to get a filtered result. Possible filter attributes are:
LmsSetupTestResult testCourseRestrictionAPI();
/** Get an unsorted List of filtered QuizData from the LMS course/quiz API
* *
* @param filterMap the FilterMap to get a filtered result. For possible filter attributes * <pre>
* see documentation on QuizData * {@link QuizData.FILTER_ATTR_QUIZ_NAME } The quiz name filter text (exclude all names that do not contain the given text)
* @return Result of an unsorted List of filtered QuizData from the LMS course/quiz API * {@link QuizData.FILTER_ATTR_START_TIME } The quiz start time (exclude all quizzes that starts before)
* </pre>
*
* @return Result of an unsorted List of filtered {@link QuizData } from the LMS course/quiz API
* or refer to an error when happened */ * or refer to an error when happened */
Result<List<QuizData>> getQuizzes(FilterMap filterMap); Result<List<QuizData>> getQuizzes(FilterMap filterMap);
/** Get all QuizData for the set of QuizData identifiers from LMS API in a collection /** Get all {@link QuizData } for the set of {@link QuizData } identifiers from LMS API in a collection
* of Result. If particular Quiz cannot be loaded because of errors or deletion, * of Result. If particular quiz cannot be loaded because of errors or deletion,
* the Result will have an error reference. * the Result will have an error reference.
* *
* @param ids the Set of Quiz identifiers to get the QuizData for * @param ids the Set of Quiz identifiers to get the {@link QuizData } for
* @return Collection of all QuizData from the given id set */ * @return Collection of all {@link QuizData } from the given id set */
Collection<Result<QuizData>> getQuizzes(Set<String> ids); Collection<Result<QuizData>> getQuizzes(Set<String> ids);
/** Get the quiz data with specified identifier. /** Get the quiz data with specified identifier.
* *
* Default implementation: Uses getQuizzes(Set<String> ids) and returns the first matching or an error. * Default implementation: Uses {@link #getQuizzes(Set<String> ids) } and returns the first matching or an error.
* *
* @param id the quiz data identifier * @param id the quiz data identifier
* @return Result refer to the quiz data or to an error when happened */ * @return Result refer to the quiz data or to an error when happened */
@ -99,26 +150,27 @@ public interface LmsAPITemplate {
.orElse(Result.ofError(new ResourceNotFoundException(EntityType.EXAM, id))); .orElse(Result.ofError(new ResourceNotFoundException(EntityType.EXAM, id)));
} }
/** Get all QuizData for the set of QuizData-identifiers (ids) from the LMS defined within the /** Get all {@link QuizData } for the set of {@link QuizData }-identifiers (ids) from the LMS defined within the
* underling LMSSetup, in a collection of Results. * underling LmsSetup, in a collection of Results.
* *
* If there is caching involved this function shall try to get the data from the cache first. * If there is caching involved this function shall try to get the data from the cache first.
* *
* NOTE: This function depends on the specific LMS implementation and on whether caching the quiz data * NOTE: This function depends on the specific LMS implementation and on whether caching the quiz data
* makes sense or not. Following strategy is recommended: * makes sense or not. Following strategy is recommended:
* Looks first in the cache if the whole set of QuizData can be get from the cache. * Looks first in the cache if the whole set of {@link QuizData } can be get from the cache.
* If all quizzes are cached, returns all from cache. * If all quizzes are cached, returns all from cache.
* If one or more quiz is not in the cache, requests all quizzes from the API and refreshes the cache * If one or more quiz is not in the cache, requests all quizzes from the API and refreshes the cache
* *
* @param ids the Set of Quiz identifiers to get the QuizData for * @param ids the Set of Quiz identifiers to get the {@link QuizData } for
* @return Collection of all QuizData from the given id set */ * @return Collection of all {@link QuizData } from the given id set */
Collection<Result<QuizData>> getQuizzesFromCache(Set<String> ids); Collection<Result<QuizData>> getQuizzesFromCache(Set<String> ids);
/** Convert an anonymous or temporary examineeUserId, sent by the SEB Client on LMS login, /** Convert an anonymous or temporary examineeUserId, sent by the SEB Client on LMS login,
* to LMS examinee account details by requesting them on the LMS API with the given examineeUserId * to LMS examinee account details by requesting them on the LMS API with the given examineeUserId
* *
* @param examineeUserId the examinee user identifier derived from SEB Client * @param examineeUserId the examinee user identifier derived from SEB Client
* @return a Result refer to the ExamineeAccountDetails instance or to an error when happened or not supported */ * @return a Result refer to the {@link ExamineeAccountDetails } instance or to an error when happened or not
* supported */
Result<ExamineeAccountDetails> getExamineeAccountDetails(String examineeUserId); Result<ExamineeAccountDetails> getExamineeAccountDetails(String examineeUserId);
/** Used to convert an anonymous or temporary examineeUserId, sent by the SEB Client on LMS login, /** Used to convert an anonymous or temporary examineeUserId, sent by the SEB Client on LMS login,
@ -142,11 +194,23 @@ public interface LmsAPITemplate {
* @return Result referencing to the Chapters model for the given course or to an error when happened. */ * @return Result referencing to the Chapters model for the given course or to an error when happened. */
Result<Chapters> getCourseChapters(String courseId); Result<Chapters> getCourseChapters(String courseId);
/** Get SEB restriction data form LMS within a SEBRestrictionData instance. The available restriction details // ****************************************************************************
// **** SEB restriction API functions *****************************************
/** Performs a test for the underling {@link LmsSetup } configuration and checks if the
* LMS and the course restriction API of the LMS can be accessed or if there are some difficulties,
* missing configuration data or connection/authentication errors.
*
* @return {@link LmsSetupTestResult } instance with the test result report */
LmsSetupTestResult testCourseRestrictionAPI();
/** Get SEB restriction data form LMS within a {@link SEBRestrictionData } instance. The available restriction
* details
* depends on the type of LMS but shall at least contains the config-key(s) and the browser-exam-key(s). * depends on the type of LMS but shall at least contains the config-key(s) and the browser-exam-key(s).
* *
* @param exam the exam to get the SEB restriction data for * @param exam the exam to get the SEB restriction data for
* @return Result refer to the SEBRestrictionData instance or to an ResourceNotFoundException if the restriction is * @return Result refer to the {@link SEBRestrictionData } instance or to an ResourceNotFoundException if the
* restriction is
* missing or to another exception on unexpected error case */ * missing or to another exception on unexpected error case */
Result<SEBRestriction> getSEBClientRestriction(Exam exam); Result<SEBRestriction> getSEBClientRestriction(Exam exam);
@ -154,7 +218,8 @@ public interface LmsAPITemplate {
* *
* @param externalExamId The exam/course identifier from LMS side (Exam.externalId) * @param externalExamId The exam/course identifier from LMS side (Exam.externalId)
* @param sebRestrictionData containing all data for SEB Client restriction to apply to the LMS * @param sebRestrictionData containing all data for SEB Client restriction to apply to the LMS
* @return Result refer to the given SEBRestrictionData if restriction was successful or to an error if not */ * @return Result refer to the given {@link SEBRestrictionData } if restriction was successful or to an error if
* not */
Result<SEBRestriction> applySEBClientRestriction( Result<SEBRestriction> applySEBClientRestriction(
String externalExamId, String externalExamId,
SEBRestriction sebRestrictionData); SEBRestriction sebRestrictionData);

View file

@ -8,9 +8,6 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.lms; package ch.ethz.seb.sebserver.webservice.servicelayer.lms;
import ch.ethz.seb.sebserver.gbl.client.ClientCredentials;
import ch.ethz.seb.sebserver.gbl.client.ProxyData;
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.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
@ -23,16 +20,10 @@ public interface LmsAPITemplateFactory {
* @return the LMS type if a specific implementation */ * @return the LMS type if a specific implementation */
LmsType lmsType(); LmsType lmsType();
/** Creates a LmsAPITemplate for the specific implements LMS type. /** Creates a {@link LmsAPITemplate } for the specific implements LMS type
* And provides it with the needed {@link APITemplateDataSupplier }
* *
* @param lmsSetup the LMS setup data to initialize the template * @param apiTemplateDataSupplier supplies all needed actual LMS setup data */
* @param credentials the access data for accessing the LMS API. Either client credentials or access token from LMS Result<LmsAPITemplate> create(final APITemplateDataSupplier apiTemplateDataSupplier);
* setup input
* @param proxyData The proxy data used to connect to the LMS if needed.
* @return Result refer to the LmsAPITemplate or to an error when happened */
Result<LmsAPITemplate> create(
final LmsSetup lmsSetup,
final ClientCredentials credentials,
final ProxyData proxyData);
} }

View file

@ -8,6 +8,8 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.lms; package ch.ethz.seb.sebserver.webservice.servicelayer.lms;
import javax.validation.constraints.NotNull;
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.SEBRestriction; import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
@ -19,6 +21,9 @@ public interface SEBRestrictionService {
String SEB_RESTRICTION_ADDITIONAL_PROPERTY_CONFIG_KEY = "config_key"; String SEB_RESTRICTION_ADDITIONAL_PROPERTY_CONFIG_KEY = "config_key";
/** Get the LmsAPIService that is used by the SEBRestrictionService */
LmsAPIService getLmsAPIService();
/** Get the SEBRestriction properties for specified Exam. /** Get the SEBRestriction properties for specified Exam.
* *
* @param exam the Exam * @param exam the Exam
@ -50,4 +55,6 @@ public interface SEBRestrictionService {
* @return Result refer to the Exam instance or to an error if happened */ * @return Result refer to the Exam instance or to an error if happened */
Result<Exam> releaseSEBClientRestriction(Exam exam); Result<Exam> releaseSEBClientRestriction(Exam exam);
boolean checkConsistency(@NotNull Long lmsSetupId, Exam exam);
} }

View file

@ -35,9 +35,9 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
* API requests in a protected environment. * API requests in a protected environment.
* *
* Extend this to implement a concrete course access API for a given type of LMS. */ * Extend this to implement a concrete course access API for a given type of LMS. */
public abstract class CourseAccess { public abstract class AbstractCourseAccess {
private static final Logger log = LoggerFactory.getLogger(CourseAccess.class); private static final Logger log = LoggerFactory.getLogger(AbstractCourseAccess.class);
/** Fetch status that indicates an asynchronous quiz data fetch status if the /** Fetch status that indicates an asynchronous quiz data fetch status if the
* concrete implementation has such. */ * concrete implementation has such. */
@ -54,7 +54,7 @@ public abstract class CourseAccess {
/** CircuitBreaker for protected examinee account details requests */ /** CircuitBreaker for protected examinee account details requests */
protected final CircuitBreaker<ExamineeAccountDetails> accountDetailRequest; protected final CircuitBreaker<ExamineeAccountDetails> accountDetailRequest;
protected CourseAccess( protected AbstractCourseAccess(
final AsyncService asyncService, final AsyncService asyncService,
final Environment environment) { final Environment environment) {

View file

@ -22,10 +22,10 @@ import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.context.event.EventListener;
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.api.EntityType;
import ch.ethz.seb.sebserver.gbl.client.ClientCredentialService; import ch.ethz.seb.sebserver.gbl.client.ClientCredentialService;
import ch.ethz.seb.sebserver.gbl.client.ClientCredentials; import ch.ethz.seb.sebserver.gbl.client.ClientCredentials;
import ch.ethz.seb.sebserver.gbl.client.ProxyData; import ch.ethz.seb.sebserver.gbl.client.ProxyData;
@ -38,6 +38,8 @@ 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.FilterMap; 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.dao.ResourceNotFoundException;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier;
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.servicelayer.lms.LmsAPITemplateFactory; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplateFactory;
@ -71,21 +73,9 @@ public class LmsAPIServiceImpl implements LmsAPIService {
this.templateFactories = new EnumMap<>(factories); this.templateFactories = new EnumMap<>(factories);
} }
/** Listen to LmsSetupChangeEvent to release an affected LmsAPITemplate from cache @Override
* public void cleanup() {
* @param event the event holding the changed LmsSetup */ this.cache.clear();
@EventListener
public void notifyLmsSetupChange(final LmsSetupChangeEvent event) {
final LmsSetup lmsSetup = event.getLmsSetup();
if (lmsSetup == null) {
return;
}
if (log.isDebugEnabled()) {
log.debug("LmsSetup changed. Update cache by removing eventually used references");
}
this.cache.remove(new CacheKey(lmsSetup.getModelId(), 0));
} }
@Override @Override
@ -107,10 +97,19 @@ public class LmsAPIServiceImpl implements LmsAPIService {
@Override @Override
public Result<LmsAPITemplate> getLmsAPITemplate(final String lmsSetupId) { public Result<LmsAPITemplate> getLmsAPITemplate(final String lmsSetupId) {
return Result.tryCatch(() -> this.lmsSetupDAO return Result.tryCatch(() -> {
.byModelId(lmsSetupId) LmsAPITemplate lmsAPITemplate = getFromCache(lmsSetupId);
.getOrThrow()) if (lmsAPITemplate == null) {
.flatMap(this::getLmsAPITemplate); lmsAPITemplate = createLmsSetupTemplate(lmsSetupId);
if (lmsAPITemplate != null) {
this.cache.put(new CacheKey(lmsSetupId, System.currentTimeMillis()), lmsAPITemplate);
}
}
if (lmsAPITemplate == null) {
throw new ResourceNotFoundException(EntityType.LMS_SETUP, lmsSetupId);
}
return lmsAPITemplate;
});
} }
@Override @Override
@ -129,23 +128,22 @@ public class LmsAPIServiceImpl implements LmsAPIService {
@Override @Override
public LmsSetupTestResult testAdHoc(final LmsSetup lmsSetup) { public LmsSetupTestResult testAdHoc(final LmsSetup lmsSetup) {
final ClientCredentials lmsCredentials = this.clientCredentialService.encryptClientCredentials( final AdHocAPITemplateDataSupplier apiTemplateDataSupplier = new AdHocAPITemplateDataSupplier(
lmsSetup.lmsAuthName, lmsSetup,
lmsSetup.lmsAuthSecret, this.clientCredentialService);
lmsSetup.lmsRestApiToken)
.getOrThrow();
final ProxyData proxyData = (StringUtils.isNoneBlank(lmsSetup.proxyHost)) final LmsAPITemplate lmsSetupTemplate = createLmsSetupTemplate(apiTemplateDataSupplier);
? new ProxyData(
lmsSetup.proxyHost,
lmsSetup.proxyPort,
this.clientCredentialService.encryptClientCredentials(
lmsSetup.proxyAuthUsername,
lmsSetup.proxyAuthSecret)
.getOrThrow())
: null;
return test(createLmsSetupTemplate(lmsSetup, lmsCredentials, proxyData)); final LmsSetupTestResult testCourseAccessAPI = lmsSetupTemplate.testCourseAccessAPI();
if (!testCourseAccessAPI.isOk()) {
return testCourseAccessAPI;
}
if (lmsSetupTemplate.lmsSetup().getLmsType().features.contains(LmsSetup.Features.SEB_RESTRICTION)) {
return lmsSetupTemplate.testCourseRestrictionAPI();
}
return LmsSetupTestResult.ofOkay();
} }
/** Collect all QuizData from all affecting LmsSetup. /** Collect all QuizData from all affecting LmsSetup.
@ -183,6 +181,7 @@ public class LmsAPIServiceImpl implements LmsAPIService {
return this.lmsSetupDAO.all(institutionId, true) return this.lmsSetupDAO.all(institutionId, true)
.getOrThrow() .getOrThrow()
.parallelStream() .parallelStream()
.map(LmsSetup::getModelId)
.map(this::getLmsAPITemplate) .map(this::getLmsAPITemplate)
.flatMap(Result::onErrorLogAndSkip) .flatMap(Result::onErrorLogAndSkip)
.map(template -> template.getQuizzes(filterMap)) .map(template -> template.getQuizzes(filterMap))
@ -193,18 +192,7 @@ public class LmsAPIServiceImpl implements LmsAPIService {
}); });
} }
private Result<LmsAPITemplate> getLmsAPITemplate(final LmsSetup lmsSetup) { private LmsAPITemplate getFromCache(final String lmsSetupId) {
return Result.tryCatch(() -> {
LmsAPITemplate lmsAPITemplate = getFromCache(lmsSetup);
if (lmsAPITemplate == null) {
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 // first cleanup the cache by removing old instances
final long currentTimeMillis = System.currentTimeMillis(); final long currentTimeMillis = System.currentTimeMillis();
new ArrayList<>(this.cache.keySet()) new ArrayList<>(this.cache.keySet())
@ -212,40 +200,103 @@ public class LmsAPIServiceImpl implements LmsAPIService {
.filter(key -> key.creationTimestamp - currentTimeMillis > Constants.DAY_IN_MILLIS) .filter(key -> key.creationTimestamp - currentTimeMillis > Constants.DAY_IN_MILLIS)
.forEach(this.cache::remove); .forEach(this.cache::remove);
// get from cache // get from cache
return this.cache.get(new CacheKey(lmsSetup.getModelId(), 0)); return this.cache.get(new CacheKey(lmsSetupId, 0));
} }
private LmsAPITemplate createLmsSetupTemplate(final LmsSetup lmsSetup) { private LmsAPITemplate createLmsSetupTemplate(final String lmsSetupId) {
if (log.isDebugEnabled()) { if (log.isDebugEnabled()) {
log.debug("Create new LmsAPITemplate for id: {}", lmsSetup.getModelId()); log.debug("Create new LmsAPITemplate for id: {}", lmsSetupId);
} }
final ClientCredentials credentials = this.lmsSetupDAO return createLmsSetupTemplate(new PersistentAPITemplateDataSupplier(
.getLmsAPIAccessCredentials(lmsSetup.getModelId()) lmsSetupId,
.getOrThrow(); this.lmsSetupDAO));
final ProxyData proxyData = this.lmsSetupDAO
.getLmsAPIAccessProxyData(lmsSetup.getModelId())
.getOr(null);
return createLmsSetupTemplate(lmsSetup, credentials, proxyData);
} }
private LmsAPITemplate createLmsSetupTemplate( private LmsAPITemplate createLmsSetupTemplate(final APITemplateDataSupplier apiTemplateDataSupplier) {
final LmsSetup lmsSetup,
final ClientCredentials credentials,
final ProxyData proxyData) {
if (!this.templateFactories.containsKey(lmsSetup.lmsType)) { final LmsType lmsType = apiTemplateDataSupplier.getLmsSetup().lmsType;
throw new UnsupportedOperationException("No support for LMS Type: " + lmsSetup.lmsType);
if (!this.templateFactories.containsKey(lmsType)) {
throw new UnsupportedOperationException("No support for LMS Type: " + lmsType);
} }
final LmsAPITemplateFactory lmsAPITemplateFactory = this.templateFactories.get(lmsSetup.lmsType); final LmsAPITemplateFactory lmsAPITemplateFactory = this.templateFactories
return lmsAPITemplateFactory.create(lmsSetup, credentials, proxyData) .get(lmsType);
return lmsAPITemplateFactory
.create(apiTemplateDataSupplier)
.getOrThrow(); .getOrThrow();
} }
/** Used to always get the actual LMS connection data from persistent */
private static final class PersistentAPITemplateDataSupplier implements APITemplateDataSupplier {
private final String lmsSetupId;
private final LmsSetupDAO lmsSetupDAO;
public PersistentAPITemplateDataSupplier(final String lmsSetupId, final LmsSetupDAO lmsSetupDAO) {
this.lmsSetupId = lmsSetupId;
this.lmsSetupDAO = lmsSetupDAO;
}
@Override
public LmsSetup getLmsSetup() {
return this.lmsSetupDAO.byModelId(this.lmsSetupId).getOrThrow();
}
@Override
public ClientCredentials getLmsClientCredentials() {
return this.lmsSetupDAO.getLmsAPIAccessCredentials(this.lmsSetupId).getOrThrow();
}
@Override
public ProxyData getProxyData() {
return this.lmsSetupDAO.getLmsAPIAccessProxyData(this.lmsSetupId).getOr(null);
}
}
/** Used to test LMS connection data that are not yet persistently stored */
private static final class AdHocAPITemplateDataSupplier implements APITemplateDataSupplier {
private final LmsSetup lmsSetup;
private final ClientCredentialService clientCredentialService;
public AdHocAPITemplateDataSupplier(
final LmsSetup lmsSetup,
final ClientCredentialService clientCredentialService) {
this.lmsSetup = lmsSetup;
this.clientCredentialService = clientCredentialService;
}
@Override
public LmsSetup getLmsSetup() {
return this.lmsSetup;
}
@Override
public ClientCredentials getLmsClientCredentials() {
return this.clientCredentialService.encryptClientCredentials(
this.lmsSetup.getLmsAuthName(),
this.lmsSetup.getLmsAuthSecret())
.getOrThrow();
}
@Override
public ProxyData getProxyData() {
return (StringUtils.isNoneBlank(this.lmsSetup.proxyHost))
? new ProxyData(
this.lmsSetup.proxyHost,
this.lmsSetup.proxyPort,
this.clientCredentialService.encryptClientCredentials(
this.lmsSetup.proxyAuthUsername,
this.lmsSetup.proxyAuthSecret)
.getOrThrow())
: null;
}
}
private static final class CacheKey { private static final class CacheKey {
final String lmsSetupId; final String lmsSetupId;
final long creationTimestamp; final long creationTimestamp;

View file

@ -1,27 +0,0 @@
/*
* 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

@ -18,6 +18,8 @@ import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.validation.constraints.NotNull;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -29,6 +31,7 @@ import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.EntityType; import ch.ethz.seb.sebserver.gbl.api.EntityType;
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.SEBRestriction; import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.Features; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.Features;
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;
@ -62,15 +65,38 @@ public class SEBRestrictionServiceImpl implements SEBRestrictionService {
this.examConfigService = examConfigService; this.examConfigService = examConfigService;
} }
@Override
public LmsAPIService getLmsAPIService() {
return this.lmsAPIService;
}
@Override
public boolean checkConsistency(@NotNull final Long lmsSetupId, final Exam exam) {
final LmsSetup lmsSetup = this.lmsAPIService
.getLmsSetup(exam.lmsSetupId)
.getOr(null);
// check only if SEB_RESTRICTION feature is on
if (lmsSetup != null && lmsSetup.lmsType.features.contains(Features.SEB_RESTRICTION)) {
if (!exam.sebRestriction) {
return false;
}
}
return true;
}
@Override @Override
@Transactional @Transactional
public Result<SEBRestriction> getSEBRestrictionFromExam(final Exam exam) { public Result<SEBRestriction> getSEBRestrictionFromExam(final Exam exam) {
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
// load the config keys from restriction and merge with new generated config keys // load the config keys from restriction and merge with new generated config keys
final long currentTimeMillis = System.currentTimeMillis();
final Set<String> configKeys = new HashSet<>(); final Set<String> configKeys = new HashSet<>();
final Collection<String> generatedKeys = this.examConfigService final Collection<String> generatedKeys = this.examConfigService
.generateConfigKeys(exam.institutionId, exam.id) .generateConfigKeys(exam.institutionId, exam.id)
.getOrThrow(); .getOrThrow();
System.out.println("******* " + (System.currentTimeMillis() - currentTimeMillis));
configKeys.addAll(generatedKeys); configKeys.addAll(generatedKeys);
if (generatedKeys != null && !generatedKeys.isEmpty()) { if (generatedKeys != null && !generatedKeys.isEmpty()) {
@ -134,6 +160,7 @@ public class SEBRestrictionServiceImpl implements SEBRestrictionService {
null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
exam.supporter, exam.supporter,
exam.status, exam.status,
null,
(browserExamKeys != null && !browserExamKeys.isEmpty()) (browserExamKeys != null && !browserExamKeys.isEmpty())
? StringUtils.join(browserExamKeys, Constants.LIST_SEPARATOR_CHAR) ? StringUtils.join(browserExamKeys, Constants.LIST_SEPARATOR_CHAR)
: StringUtils.EMPTY, : StringUtils.EMPTY,
@ -167,28 +194,31 @@ public class SEBRestrictionServiceImpl implements SEBRestrictionService {
@Override @Override
public Result<Exam> applySEBClientRestriction(final Exam exam) { public Result<Exam> applySEBClientRestriction(final Exam exam) {
if (!this.lmsAPIService return Result.tryCatch(() -> {
.getLmsSetup(exam.lmsSetupId) if (!this.lmsAPIService
.getOrThrow().lmsType.features.contains(Features.SEB_RESTRICTION)) { .getLmsSetup(exam.lmsSetupId)
.getOrThrow().lmsType.features.contains(Features.SEB_RESTRICTION)) {
return Result.of(exam); return exam;
} }
return this.getSEBRestrictionFromExam(exam) return this.getSEBRestrictionFromExam(exam)
.map(sebRestrictionData -> { .map(sebRestrictionData -> {
if (log.isDebugEnabled()) { if (log.isDebugEnabled()) {
log.debug("Applying SEB Client restriction on LMS with: {}", sebRestrictionData); log.debug("Applying SEB Client restriction on LMS with: {}", sebRestrictionData);
} }
return this.lmsAPIService return this.lmsAPIService
.getLmsAPITemplate(exam.lmsSetupId) .getLmsAPITemplate(exam.lmsSetupId)
.flatMap(lmsTemplate -> lmsTemplate.applySEBClientRestriction( .flatMap(lmsTemplate -> lmsTemplate.applySEBClientRestriction(
exam.externalId, exam.externalId,
sebRestrictionData)) sebRestrictionData))
.map(data -> exam) .map(data -> exam)
.getOrThrow(); .getOrThrow();
}); })
.getOrThrow();
});
} }
@Override @Override

View file

@ -10,6 +10,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.edx;
import java.net.URL; import java.net.URL;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -53,12 +54,13 @@ 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.WebserviceInfo; import ch.ethz.seb.sebserver.webservice.WebserviceInfo;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.CourseAccess; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.AbstractCourseAccess;
/** Implements the LmsAPITemplate for Open edX LMS Course API access. /** Implements the LmsAPITemplate for Open edX LMS Course API access.
* *
* See also: https://course-catalog-api-guide.readthedocs.io */ * See also: https://course-catalog-api-guide.readthedocs.io */
final class OpenEdxCourseAccess extends CourseAccess { final class OpenEdxCourseAccess extends AbstractCourseAccess {
private static final Logger log = LoggerFactory.getLogger(OpenEdxCourseAccess.class); private static final Logger log = LoggerFactory.getLogger(OpenEdxCourseAccess.class);
@ -70,7 +72,6 @@ final class OpenEdxCourseAccess extends CourseAccess {
private static final String OPEN_EDX_DEFAULT_USER_PROFILE_ENDPOINT = "/api/user/v1/accounts?username="; private static final String OPEN_EDX_DEFAULT_USER_PROFILE_ENDPOINT = "/api/user/v1/accounts?username=";
private final JSONMapper jsonMapper; private final JSONMapper jsonMapper;
private final LmsSetup lmsSetup;
private final OpenEdxRestTemplateFactory openEdxRestTemplateFactory; private final OpenEdxRestTemplateFactory openEdxRestTemplateFactory;
private final WebserviceInfo webserviceInfo; private final WebserviceInfo webserviceInfo;
private final MemoizingCircuitBreaker<List<QuizData>> allQuizzesRequest; private final MemoizingCircuitBreaker<List<QuizData>> allQuizzesRequest;
@ -80,7 +81,6 @@ final class OpenEdxCourseAccess extends CourseAccess {
public OpenEdxCourseAccess( public OpenEdxCourseAccess(
final JSONMapper jsonMapper, final JSONMapper jsonMapper,
final LmsSetup lmsSetup,
final OpenEdxRestTemplateFactory openEdxRestTemplateFactory, final OpenEdxRestTemplateFactory openEdxRestTemplateFactory,
final WebserviceInfo webserviceInfo, final WebserviceInfo webserviceInfo,
final AsyncService asyncService, final AsyncService asyncService,
@ -88,7 +88,6 @@ final class OpenEdxCourseAccess extends CourseAccess {
super(asyncService, environment); super(asyncService, environment);
this.jsonMapper = jsonMapper; this.jsonMapper = jsonMapper;
this.lmsSetup = lmsSetup;
this.openEdxRestTemplateFactory = openEdxRestTemplateFactory; this.openEdxRestTemplateFactory = openEdxRestTemplateFactory;
this.webserviceInfo = webserviceInfo; this.webserviceInfo = webserviceInfo;
@ -130,8 +129,14 @@ final class OpenEdxCourseAccess extends CourseAccess {
}; };
} }
APITemplateDataSupplier getApiTemplateDataSupplier() {
return this.openEdxRestTemplateFactory.apiTemplateDataSupplier;
}
LmsSetupTestResult initAPIAccess() { LmsSetupTestResult initAPIAccess() {
final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup();
final LmsSetupTestResult attributesCheck = this.openEdxRestTemplateFactory.test(); final LmsSetupTestResult attributesCheck = this.openEdxRestTemplateFactory.test();
if (!attributesCheck.isOk()) { if (!attributesCheck.isOk()) {
return attributesCheck; return attributesCheck;
@ -148,13 +153,14 @@ final class OpenEdxCourseAccess extends CourseAccess {
final OAuth2RestTemplate restTemplate = restTemplateRequest.get(); final OAuth2RestTemplate restTemplate = restTemplateRequest.get();
try { try {
this.getEdxPage(this.lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_ENDPOINT, restTemplate); restTemplate.getAccessToken();
//this.getEdxPage(lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_ENDPOINT, restTemplate);
} catch (final RuntimeException e) { } catch (final RuntimeException e) {
restTemplate.setAuthenticator(new EdxOAuth2RequestAuthenticator()); restTemplate.setAuthenticator(new EdxOAuth2RequestAuthenticator());
try { try {
this.getEdxPage(this.lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_ENDPOINT, restTemplate); this.getEdxPage(lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_ENDPOINT, restTemplate);
} catch (final RuntimeException ee) { } catch (final RuntimeException ee) {
log.error("Failed to access Open edX course API: ", ee); log.error("Failed to access Open edX course API: ", ee);
return LmsSetupTestResult.ofQuizAccessAPIError(ee.getMessage()); return LmsSetupTestResult.ofQuizAccessAPIError(ee.getMessage());
@ -167,14 +173,18 @@ final class OpenEdxCourseAccess extends CourseAccess {
@Override @Override
public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeSessionId) { public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeSessionId) {
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup();
final HttpHeaders httpHeaders = new HttpHeaders();
final OAuth2RestTemplate template = getRestTemplate() final OAuth2RestTemplate template = getRestTemplate()
.getOrThrow(); .getOrThrow();
final String externalStartURI = this.webserviceInfo.getLmsExternalAddressAlias(this.lmsSetup.lmsApiUrl); final String externalStartURI = this.webserviceInfo
.getLmsExternalAddressAlias(lmsSetup.lmsApiUrl);
final String uri = (externalStartURI != null) final String uri = (externalStartURI != null)
? externalStartURI + OPEN_EDX_DEFAULT_USER_PROFILE_ENDPOINT + examineeSessionId ? externalStartURI + OPEN_EDX_DEFAULT_USER_PROFILE_ENDPOINT + examineeSessionId
: this.lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_USER_PROFILE_ENDPOINT + examineeSessionId; : lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_USER_PROFILE_ENDPOINT + examineeSessionId;
final HttpHeaders httpHeaders = new HttpHeaders();
final String responseJSON = template.exchange( final String responseJSON = template.exchange(
uri, uri,
HttpMethod.GET, HttpMethod.GET,
@ -211,9 +221,24 @@ final class OpenEdxCourseAccess extends CourseAccess {
@Override @Override
protected Supplier<List<QuizData>> quizzesSupplier(final Set<String> ids) { protected Supplier<List<QuizData>> quizzesSupplier(final Set<String> ids) {
return () -> getRestTemplate()
.map(template -> this.collectQuizzes(template, ids)) if (ids.size() == 1) {
.getOrThrow(); return () -> {
final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup();
final String externalStartURI = getExternalLMSServerAddress(lmsSetup);
return Arrays.asList(quizDataOf(
lmsSetup,
getOneCourses(
lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_ENDPOINT,
getRestTemplate().getOrThrow(),
ids.iterator().next()),
externalStartURI));
};
} else {
return () -> getRestTemplate()
.map(template -> this.collectQuizzes(template, ids))
.getOrThrow();
}
} }
private Supplier<List<QuizData>> quizzesSupplier() { private Supplier<List<QuizData>> quizzesSupplier() {
@ -225,8 +250,10 @@ final class OpenEdxCourseAccess extends CourseAccess {
@Override @Override
protected Supplier<Chapters> getCourseChaptersSupplier(final String courseId) { protected Supplier<Chapters> getCourseChaptersSupplier(final String courseId) {
return () -> { return () -> {
final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup();
final String uri = final String uri =
this.lmsSetup.lmsApiUrl + lmsSetup.lmsApiUrl +
OPEN_EDX_DEFAULT_BLOCKS_ENDPOINT + OPEN_EDX_DEFAULT_BLOCKS_ENDPOINT +
Utils.encodeFormURL_UTF_8(courseId); Utils.encodeFormURL_UTF_8(courseId);
return new Chapters(getCourseBlocks(uri) return new Chapters(getCourseBlocks(uri)
@ -239,16 +266,18 @@ final class OpenEdxCourseAccess extends CourseAccess {
} }
private ArrayList<QuizData> collectQuizzes(final OAuth2RestTemplate restTemplate, final Set<String> ids) { private ArrayList<QuizData> collectQuizzes(final OAuth2RestTemplate restTemplate, final Set<String> ids) {
final String externalStartURI = getExternalLMSServerAddress(this.lmsSetup); final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup();
final String externalStartURI = getExternalLMSServerAddress(lmsSetup);
return collectCourses( return collectCourses(
this.lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_ENDPOINT, lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_ENDPOINT,
restTemplate, restTemplate,
ids) ids)
.stream() .stream()
.reduce( .reduce(
new ArrayList<>(), new ArrayList<>(),
(list, courseData) -> { (list, courseData) -> {
list.add(quizDataOf(this.lmsSetup, courseData, externalStartURI)); list.add(quizDataOf(lmsSetup, courseData, externalStartURI));
return list; return list;
}, },
(list1, list2) -> { (list1, list2) -> {
@ -258,15 +287,16 @@ final class OpenEdxCourseAccess extends CourseAccess {
} }
private ArrayList<QuizData> collectAllQuizzes(final OAuth2RestTemplate restTemplate) { private ArrayList<QuizData> collectAllQuizzes(final OAuth2RestTemplate restTemplate) {
final String externalStartURI = getExternalLMSServerAddress(this.lmsSetup); final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup();
final String externalStartURI = getExternalLMSServerAddress(lmsSetup);
return collectAllCourses( return collectAllCourses(
this.lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_ENDPOINT, lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_ENDPOINT,
restTemplate) restTemplate)
.stream() .stream()
.reduce( .reduce(
new ArrayList<>(), new ArrayList<>(),
(list, courseData) -> { (list, courseData) -> {
list.add(quizDataOf(this.lmsSetup, courseData, externalStartURI)); list.add(quizDataOf(lmsSetup, courseData, externalStartURI));
return list; return list;
}, },
(list1, list2) -> { (list1, list2) -> {
@ -322,6 +352,21 @@ final class OpenEdxCourseAccess extends CourseAccess {
return collector; return collector;
} }
private CourseData getOneCourses(
final String pageURI,
final OAuth2RestTemplate restTemplate,
final String id) {
final HttpHeaders httpHeaders = new HttpHeaders();
final ResponseEntity<CourseData> exchange = restTemplate.exchange(
pageURI + "/" + id,
HttpMethod.GET,
new HttpEntity<>(httpHeaders),
CourseData.class);
return exchange.getBody();
}
private List<CourseData> collectAllCourses(final String pageURI, final OAuth2RestTemplate restTemplate) { private List<CourseData> collectAllCourses(final String pageURI, final OAuth2RestTemplate restTemplate) {
final List<CourseData> collector = new ArrayList<>(); final List<CourseData> collector = new ArrayList<>();
EdXPage page = getEdxPage(pageURI, restTemplate).getBody(); EdXPage page = getEdxPage(pageURI, restTemplate).getBody();

View file

@ -8,8 +8,6 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.edx; package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.edx;
import java.util.function.BooleanSupplier;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.http.HttpEntity; import org.springframework.http.HttpEntity;
@ -23,8 +21,6 @@ import org.springframework.web.client.HttpClientErrorException;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.api.APIMessage.APIMessageException;
import ch.ethz.seb.sebserver.gbl.api.JSONMapper; import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
import ch.ethz.seb.sebserver.gbl.model.exam.OpenEdxSEBRestriction; import ch.ethz.seb.sebserver.gbl.model.exam.OpenEdxSEBRestriction;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
@ -43,19 +39,16 @@ public class OpenEdxCourseRestriction {
private static final String OPEN_EDX_DEFAULT_COURSE_RESTRICTION_API_PATH = private static final String OPEN_EDX_DEFAULT_COURSE_RESTRICTION_API_PATH =
"/seb-openedx/api/v1/course/%s/configuration/"; "/seb-openedx/api/v1/course/%s/configuration/";
private final LmsSetup lmsSetup;
private final JSONMapper jsonMapper; private final JSONMapper jsonMapper;
private final OpenEdxRestTemplateFactory openEdxRestTemplateFactory; private final OpenEdxRestTemplateFactory openEdxRestTemplateFactory;
private OAuth2RestTemplate restTemplate; private OAuth2RestTemplate restTemplate;
protected OpenEdxCourseRestriction( protected OpenEdxCourseRestriction(
final LmsSetup lmsSetup,
final JSONMapper jsonMapper, final JSONMapper jsonMapper,
final OpenEdxRestTemplateFactory openEdxRestTemplateFactory, final OpenEdxRestTemplateFactory openEdxRestTemplateFactory,
final int restrictionAPIPushCount) { final int restrictionAPIPushCount) {
this.lmsSetup = lmsSetup;
this.jsonMapper = jsonMapper; this.jsonMapper = jsonMapper;
this.openEdxRestTemplateFactory = openEdxRestTemplateFactory; this.openEdxRestTemplateFactory = openEdxRestTemplateFactory;
} }
@ -75,15 +68,16 @@ public class OpenEdxCourseRestriction {
} }
final OAuth2RestTemplate restTemplate = restTemplateRequest.get(); final OAuth2RestTemplate restTemplate = restTemplateRequest.get();
// NOTE: since the OPEN_EDX_DEFAULT_COURSE_RESTRICTION_API_INFO endpoint is
// not accessible within OAuth2 authentication (just with user - authentication),
// we can only check if the endpoint is available for now. This is checked
// if there is no 404 response.
// TODO: Ask eduNEXT to implement also OAuth2 API access for this endpoint to be able
// to check the version of the installed plugin.
final String url = this.lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_RESTRICTION_API_INFO;
try { try {
final LmsSetup lmsSetup = this.openEdxRestTemplateFactory.apiTemplateDataSupplier.getLmsSetup();
// NOTE: since the OPEN_EDX_DEFAULT_COURSE_RESTRICTION_API_INFO endpoint is
// not accessible within OAuth2 authentication (just with user - authentication),
// we can only check if the endpoint is available for now. This is checked
// if there is no 404 response.
// TODO: Ask eduNEXT to implement also OAuth2 API access for this endpoint to be able
// to check the version of the installed plugin.
final String url = lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_RESTRICTION_API_INFO;
restTemplate.exchange( restTemplate.exchange(
url, url,
@ -111,8 +105,10 @@ public class OpenEdxCourseRestriction {
log.debug("GET SEB Client restriction on course: {}", courseId); log.debug("GET SEB Client restriction on course: {}", courseId);
} }
final LmsSetup lmsSetup = this.openEdxRestTemplateFactory.apiTemplateDataSupplier.getLmsSetup();
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
final String url = this.lmsSetup.lmsApiUrl + getSEBRestrictionUrl(courseId); final String url = lmsSetup.lmsApiUrl + getSEBRestrictionUrl(courseId);
final HttpHeaders httpHeaders = new HttpHeaders(); final HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); httpHeaders.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
httpHeaders.add(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate"); httpHeaders.add(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate");
@ -146,9 +142,28 @@ public class OpenEdxCourseRestriction {
log.debug("PUT SEB Client restriction on course: {} : {}", courseId, restriction); log.debug("PUT SEB Client restriction on course: {} : {}", courseId, restriction);
} }
return handleSEBRestriction(pushSEBRestrictionFunction( return Result.tryCatch(() -> {
restriction, final LmsSetup lmsSetup = this.openEdxRestTemplateFactory.apiTemplateDataSupplier.getLmsSetup();
courseId)); final String url = lmsSetup.lmsApiUrl + getSEBRestrictionUrl(courseId);
final HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
httpHeaders.add(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate");
final OpenEdxSEBRestriction body = this
.getRestTemplate()
.getOrThrow()
.exchange(
url,
HttpMethod.PUT,
new HttpEntity<>(toJson(restriction), httpHeaders),
OpenEdxSEBRestriction.class)
.getBody();
if (log.isDebugEnabled()) {
log.debug("Successfully PUT SEB Client restriction on course: {} : {}", courseId, body);
}
return true;
});
} }
Result<Boolean> deleteSEBRestriction(final String courseId) { Result<Boolean> deleteSEBRestriction(final String courseId) {
@ -157,74 +172,99 @@ public class OpenEdxCourseRestriction {
log.debug("DELETE SEB Client restriction on course: {}", courseId); log.debug("DELETE SEB Client restriction on course: {}", courseId);
} }
return handleSEBRestriction(deleteSEBRestrictionFunction(courseId)); return Result.tryCatch(() -> {
} final LmsSetup lmsSetup = this.openEdxRestTemplateFactory.apiTemplateDataSupplier.getLmsSetup();
final String url = lmsSetup.lmsApiUrl + getSEBRestrictionUrl(courseId);
private BooleanSupplier pushSEBRestrictionFunction(
final OpenEdxSEBRestriction restriction,
final String courseId) {
final String url = this.lmsSetup.lmsApiUrl + getSEBRestrictionUrl(courseId);
final HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
httpHeaders.add(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate");
return () -> {
final OpenEdxSEBRestriction body = this.restTemplate.exchange(
url,
HttpMethod.PUT,
new HttpEntity<>(toJson(restriction), httpHeaders),
OpenEdxSEBRestriction.class)
.getBody();
if (log.isDebugEnabled()) {
log.debug("Successfully PUT SEB Client restriction on course: {} : {}", courseId, body);
}
return true;
};
}
private BooleanSupplier deleteSEBRestrictionFunction(final String courseId) {
final String url = this.lmsSetup.lmsApiUrl + getSEBRestrictionUrl(courseId);
return () -> {
final HttpHeaders httpHeaders = new HttpHeaders(); final HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate"); httpHeaders.add(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate");
final ResponseEntity<Object> exchange = this.restTemplate.exchange( final ResponseEntity<Object> exchange = this
url, .getRestTemplate()
HttpMethod.DELETE, .getOrThrow()
new HttpEntity<>(httpHeaders), .exchange(
Object.class); url,
HttpMethod.DELETE,
new HttpEntity<>(httpHeaders),
Object.class);
if (exchange.getStatusCode() == HttpStatus.NO_CONTENT) { if (exchange.getStatusCode() == HttpStatus.NO_CONTENT) {
if (log.isDebugEnabled()) { if (log.isDebugEnabled()) {
log.debug("Successfully PUT SEB Client restriction on course: {}", courseId); log.debug("Successfully PUT SEB Client restriction on course: {}", courseId);
} }
return true;
} else { } else {
log.error("Unexpected response for deletion: {}", exchange); throw new RuntimeException("Unexpected response for deletion: " + exchange);
return false;
} }
});
return true;
};
} }
private Result<Boolean> handleSEBRestriction(final BooleanSupplier task) { // private BooleanSupplier pushSEBRestrictionFunction(
return getRestTemplate() // final OpenEdxSEBRestriction restriction,
.map(restTemplate -> { // final String courseId) {
try { //
return task.getAsBoolean(); // final LmsSetup lmsSetup = this.openEdxRestTemplateFactory.apiTemplateDataSupplier.getLmsSetup();
} catch (final HttpClientErrorException ce) { // final String url = lmsSetup.lmsApiUrl + getSEBRestrictionUrl(courseId);
if (ce.getStatusCode() == HttpStatus.UNAUTHORIZED) { // final HttpHeaders httpHeaders = new HttpHeaders();
throw new APIMessageException(APIMessage.ErrorMessage.UNAUTHORIZED.of(ce.getMessage() // httpHeaders.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
+ " Unable to get access for API. Please check the corresponding LMS Setup ")); // httpHeaders.add(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate");
} // return () -> {
throw ce; // final OpenEdxSEBRestriction body = this.restTemplate.exchange(
} catch (final Exception e) { // url,
throw new RuntimeException("Unexpected: ", e); // HttpMethod.PUT,
} // new HttpEntity<>(toJson(restriction), httpHeaders),
}); // OpenEdxSEBRestriction.class)
} // .getBody();
//
// if (log.isDebugEnabled()) {
// log.debug("Successfully PUT SEB Client restriction on course: {} : {}", courseId, body);
// }
//
// return true;
// };
// }
// private BooleanSupplier deleteSEBRestrictionFunction(final String courseId) {
//
// final LmsSetup lmsSetup = this.openEdxRestTemplateFactory.apiTemplateDataSupplier.getLmsSetup();
// final String url = lmsSetup.lmsApiUrl + getSEBRestrictionUrl(courseId);
// return () -> {
// final HttpHeaders httpHeaders = new HttpHeaders();
// httpHeaders.add(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate");
// final ResponseEntity<Object> exchange = this.restTemplate.exchange(
// url,
// HttpMethod.DELETE,
// new HttpEntity<>(httpHeaders),
// Object.class);
//
// if (exchange.getStatusCode() == HttpStatus.NO_CONTENT) {
// if (log.isDebugEnabled()) {
// log.debug("Successfully PUT SEB Client restriction on course: {}", courseId);
// }
// } else {
// log.error("Unexpected response for deletion: {}", exchange);
// return false;
// }
//
// return true;
// };
// }
// private Result<Boolean> handleSEBRestriction(final BooleanSupplier task) {
// return getRestTemplate()
// .map(restTemplate -> {
// try {
// return task.getAsBoolean();
// } catch (final HttpClientErrorException ce) {
// if (ce.getStatusCode() == HttpStatus.UNAUTHORIZED) {
// throw new APIMessageException(APIMessage.ErrorMessage.UNAUTHORIZED.of(ce.getMessage()
// + " Unable to get access for API. Please check the corresponding LMS Setup "));
// }
// throw ce;
// } catch (final Exception e) {
// throw new RuntimeException("Unexpected: ", e);
// }
// });
// }
private String getSEBRestrictionUrl(final String courseId) { private String getSEBRestrictionUrl(final String courseId) {
return String.format(OPEN_EDX_DEFAULT_COURSE_RESTRICTION_API_PATH, courseId); return String.format(OPEN_EDX_DEFAULT_COURSE_RESTRICTION_API_PATH, courseId);

View file

@ -24,6 +24,7 @@ import ch.ethz.seb.sebserver.gbl.model.exam.OpenEdxSEBRestriction;
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.exam.SEBRestriction; import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction;
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;
@ -39,23 +40,27 @@ final class OpenEdxLmsAPITemplate implements LmsAPITemplate {
private static final Logger log = LoggerFactory.getLogger(OpenEdxLmsAPITemplate.class); private static final Logger log = LoggerFactory.getLogger(OpenEdxLmsAPITemplate.class);
private final LmsSetup lmsSetup;
private final OpenEdxCourseAccess openEdxCourseAccess; private final OpenEdxCourseAccess openEdxCourseAccess;
private final OpenEdxCourseRestriction openEdxCourseRestriction; private final OpenEdxCourseRestriction openEdxCourseRestriction;
OpenEdxLmsAPITemplate( OpenEdxLmsAPITemplate(
final LmsSetup lmsSetup,
final OpenEdxCourseAccess openEdxCourseAccess, final OpenEdxCourseAccess openEdxCourseAccess,
final OpenEdxCourseRestriction openEdxCourseRestriction) { final OpenEdxCourseRestriction openEdxCourseRestriction) {
this.lmsSetup = lmsSetup;
this.openEdxCourseAccess = openEdxCourseAccess; this.openEdxCourseAccess = openEdxCourseAccess;
this.openEdxCourseRestriction = openEdxCourseRestriction; this.openEdxCourseRestriction = openEdxCourseRestriction;
} }
@Override
public LmsType getType() {
return LmsType.OPEN_EDX;
}
@Override @Override
public LmsSetup lmsSetup() { public LmsSetup lmsSetup() {
return this.lmsSetup; return this.openEdxCourseAccess
.getApiTemplateDataSupplier()
.getLmsSetup();
} }
@Override @Override
@ -150,7 +155,8 @@ final class OpenEdxLmsAPITemplate implements LmsAPITemplate {
log.debug("Release SEB Client restriction for Exam: {}", exam); log.debug("Release SEB Client restriction for Exam: {}", exam);
} }
return this.openEdxCourseRestriction.deleteSEBRestriction(exam.externalId) return this.openEdxCourseRestriction
.deleteSEBRestriction(exam.externalId)
.map(result -> exam); .map(result -> exam);
} }

View file

@ -19,13 +19,11 @@ import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.JSONMapper; import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
import ch.ethz.seb.sebserver.gbl.async.AsyncService; import ch.ethz.seb.sebserver.gbl.async.AsyncService;
import ch.ethz.seb.sebserver.gbl.client.ClientCredentialService; import ch.ethz.seb.sebserver.gbl.client.ClientCredentialService;
import ch.ethz.seb.sebserver.gbl.client.ClientCredentials;
import ch.ethz.seb.sebserver.gbl.client.ProxyData;
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.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.WebserviceInfo; import ch.ethz.seb.sebserver.webservice.WebserviceInfo;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplateFactory; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplateFactory;
@ -71,37 +69,29 @@ public class OpenEdxLmsAPITemplateFactory implements LmsAPITemplateFactory {
} }
@Override @Override
public Result<LmsAPITemplate> create( public Result<LmsAPITemplate> create(final APITemplateDataSupplier apiTemplateDataSupplier) {
final LmsSetup lmsSetup,
final ClientCredentials credentials,
final ProxyData proxyData) {
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
final OpenEdxRestTemplateFactory openEdxRestTemplateFactory = new OpenEdxRestTemplateFactory( final OpenEdxRestTemplateFactory openEdxRestTemplateFactory = new OpenEdxRestTemplateFactory(
lmsSetup, apiTemplateDataSupplier,
credentials,
proxyData,
this.clientCredentialService, this.clientCredentialService,
this.clientHttpRequestFactoryService, this.clientHttpRequestFactoryService,
this.alternativeTokenRequestPaths); this.alternativeTokenRequestPaths);
final OpenEdxCourseAccess openEdxCourseAccess = new OpenEdxCourseAccess( final OpenEdxCourseAccess openEdxCourseAccess = new OpenEdxCourseAccess(
this.jsonMapper, this.jsonMapper,
lmsSetup,
openEdxRestTemplateFactory, openEdxRestTemplateFactory,
this.webserviceInfo, this.webserviceInfo,
this.asyncService, this.asyncService,
this.environment); this.environment);
final OpenEdxCourseRestriction openEdxCourseRestriction = new OpenEdxCourseRestriction( final OpenEdxCourseRestriction openEdxCourseRestriction = new OpenEdxCourseRestriction(
lmsSetup,
this.jsonMapper, this.jsonMapper,
openEdxRestTemplateFactory, openEdxRestTemplateFactory,
this.restrictionAPIPushCount); this.restrictionAPIPushCount);
return new OpenEdxLmsAPITemplate( return new OpenEdxLmsAPITemplate(
lmsSetup,
openEdxCourseAccess, openEdxCourseAccess,
openEdxCourseRestriction); openEdxCourseRestriction);
}); });

View file

@ -43,30 +43,25 @@ 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.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.lms.APITemplateDataSupplier;
final class OpenEdxRestTemplateFactory { final class OpenEdxRestTemplateFactory {
private static final String OPEN_EDX_DEFAULT_TOKEN_REQUEST_PATH = "/oauth2/access_token"; private static final String OPEN_EDX_DEFAULT_TOKEN_REQUEST_PATH = "/oauth2/access_token";
final LmsSetup lmsSetup; final APITemplateDataSupplier apiTemplateDataSupplier;
final ClientCredentials credentials;
final ProxyData proxyData;
final ClientHttpRequestFactoryService clientHttpRequestFactoryService; final ClientHttpRequestFactoryService clientHttpRequestFactoryService;
final ClientCredentialService clientCredentialService; final ClientCredentialService clientCredentialService;
final Set<String> knownTokenAccessPaths; final Set<String> knownTokenAccessPaths;
OpenEdxRestTemplateFactory( OpenEdxRestTemplateFactory(
final LmsSetup lmsSetup, final APITemplateDataSupplier apiTemplateDataSupplier,
final ClientCredentials credentials,
final ProxyData proxyData,
final ClientCredentialService clientCredentialService, final ClientCredentialService clientCredentialService,
final ClientHttpRequestFactoryService clientHttpRequestFactoryService, final ClientHttpRequestFactoryService clientHttpRequestFactoryService,
final String[] alternativeTokenRequestPaths) { final String[] alternativeTokenRequestPaths) {
this.lmsSetup = lmsSetup; this.apiTemplateDataSupplier = apiTemplateDataSupplier;
this.clientCredentialService = clientCredentialService; this.clientCredentialService = clientCredentialService;
this.credentials = credentials;
this.proxyData = proxyData;
this.clientHttpRequestFactoryService = clientHttpRequestFactoryService; this.clientHttpRequestFactoryService = clientHttpRequestFactoryService;
this.knownTokenAccessPaths = new HashSet<>(); this.knownTokenAccessPaths = new HashSet<>();
@ -76,26 +71,34 @@ final class OpenEdxRestTemplateFactory {
} }
} }
APITemplateDataSupplier getApiTemplateDataSupplier() {
return this.apiTemplateDataSupplier;
}
public LmsSetupTestResult test() { public LmsSetupTestResult test() {
final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup();
final ClientCredentials lmsClientCredentials = this.apiTemplateDataSupplier.getLmsClientCredentials();
final List<APIMessage> missingAttrs = new ArrayList<>(); final List<APIMessage> missingAttrs = new ArrayList<>();
if (StringUtils.isBlank(this.lmsSetup.lmsApiUrl)) { if (StringUtils.isBlank(lmsSetup.lmsApiUrl)) {
missingAttrs.add(APIMessage.fieldValidationError( missingAttrs.add(APIMessage.fieldValidationError(
LMS_SETUP.ATTR_LMS_URL, LMS_SETUP.ATTR_LMS_URL,
"lmsSetup:lmsUrl:notNull")); "lmsSetup:lmsUrl:notNull"));
} else { } else {
// try to connect to the url // try to connect to the url
if (!Utils.pingHost(this.lmsSetup.lmsApiUrl)) { if (!Utils.pingHost(lmsSetup.lmsApiUrl)) {
missingAttrs.add(APIMessage.fieldValidationError( missingAttrs.add(APIMessage.fieldValidationError(
LMS_SETUP.ATTR_LMS_URL, LMS_SETUP.ATTR_LMS_URL,
"lmsSetup:lmsUrl:url.invalid")); "lmsSetup:lmsUrl:url.invalid"));
} }
} }
if (!this.credentials.hasClientId()) { if (!lmsClientCredentials.hasClientId()) {
missingAttrs.add(APIMessage.fieldValidationError( missingAttrs.add(APIMessage.fieldValidationError(
LMS_SETUP.ATTR_LMS_CLIENTNAME, LMS_SETUP.ATTR_LMS_CLIENTNAME,
"lmsSetup:lmsClientname:notNull")); "lmsSetup:lmsClientname:notNull"));
} }
if (!this.credentials.hasSecret()) { if (!lmsClientCredentials.hasSecret()) {
missingAttrs.add(APIMessage.fieldValidationError( missingAttrs.add(APIMessage.fieldValidationError(
LMS_SETUP.ATTR_LMS_CLIENTSECRET, LMS_SETUP.ATTR_LMS_CLIENTSECRET,
"lmsSetup:lmsClientsecret:notNull")); "lmsSetup:lmsClientsecret:notNull"));
@ -120,10 +123,7 @@ final class OpenEdxRestTemplateFactory {
Result<OAuth2RestTemplate> createOAuthRestTemplate(final String accessTokenPath) { Result<OAuth2RestTemplate> createOAuthRestTemplate(final String accessTokenPath) {
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
final OAuth2RestTemplate template = createRestTemplate( final OAuth2RestTemplate template = createRestTemplate(accessTokenPath);
this.lmsSetup,
this.credentials,
accessTokenPath);
final OAuth2AccessToken accessToken = template.getAccessToken(); final OAuth2AccessToken accessToken = template.getAccessToken();
if (accessToken == null) { if (accessToken == null) {
@ -134,10 +134,11 @@ final class OpenEdxRestTemplateFactory {
}); });
} }
private OAuth2RestTemplate createRestTemplate( private OAuth2RestTemplate createRestTemplate(final String accessTokenRequestPath) throws URISyntaxException {
final LmsSetup lmsSetup,
final ClientCredentials credentials, final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup();
final String accessTokenRequestPath) throws URISyntaxException { final ClientCredentials credentials = this.apiTemplateDataSupplier.getLmsClientCredentials();
final ProxyData proxyData = this.apiTemplateDataSupplier.getProxyData();
final CharSequence plainClientId = credentials.clientId; final CharSequence plainClientId = credentials.clientId;
final CharSequence plainClientSecret = this.clientCredentialService final CharSequence plainClientSecret = this.clientCredentialService
@ -150,7 +151,7 @@ final class OpenEdxRestTemplateFactory {
details.setClientSecret(plainClientSecret.toString()); details.setClientSecret(plainClientSecret.toString());
final ClientHttpRequestFactory clientHttpRequestFactory = this.clientHttpRequestFactoryService final ClientHttpRequestFactory clientHttpRequestFactory = this.clientHttpRequestFactoryService
.getClientHttpRequestFactory(this.proxyData) .getClientHttpRequestFactory(proxyData)
.getOrThrow(); .getOrThrow();
final OAuth2RestTemplate template = new OAuth2RestTemplate(details); final OAuth2RestTemplate template = new OAuth2RestTemplate(details);

View file

@ -6,18 +6,16 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/ */
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl; package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.mockup;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import ch.ethz.seb.sebserver.gbl.client.ClientCredentials;
import ch.ethz.seb.sebserver.gbl.client.ProxyData;
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.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.WebserviceInfo; import ch.ethz.seb.sebserver.webservice.WebserviceInfo;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplateFactory; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplateFactory;
@ -38,14 +36,9 @@ public class MockLmsAPITemplateFactory implements LmsAPITemplateFactory {
} }
@Override @Override
public Result<LmsAPITemplate> create( public Result<LmsAPITemplate> create(final APITemplateDataSupplier apiTemplateDataSupplier) {
final LmsSetup lmsSetup,
final ClientCredentials credentials,
final ProxyData proxyData) {
return Result.tryCatch(() -> new MockupLmsAPITemplate( return Result.tryCatch(() -> new MockupLmsAPITemplate(
lmsSetup, apiTemplateDataSupplier,
credentials,
this.webserviceInfo)); this.webserviceInfo));
} }

View file

@ -6,7 +6,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/ */
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl; package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.mockup;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
@ -35,31 +35,32 @@ 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.WebserviceInfo; import ch.ethz.seb.sebserver.webservice.WebserviceInfo;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier;
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.servicelayer.lms.impl.NoSEBRestrictionException;
final class MockupLmsAPITemplate implements LmsAPITemplate { public class MockupLmsAPITemplate implements LmsAPITemplate {
private static final Logger log = LoggerFactory.getLogger(MockupLmsAPITemplate.class); private static final Logger log = LoggerFactory.getLogger(MockupLmsAPITemplate.class);
private final LmsSetup lmsSetup;
private final ClientCredentials credentials;
private final Collection<QuizData> mockups; private final Collection<QuizData> mockups;
private final WebserviceInfo webserviceInfo; private final WebserviceInfo webserviceInfo;
private final APITemplateDataSupplier apiTemplateDataSupplier;
MockupLmsAPITemplate( MockupLmsAPITemplate(
final LmsSetup lmsSetup, final APITemplateDataSupplier apiTemplateDataSupplier,
final ClientCredentials credentials,
final WebserviceInfo webserviceInfo) { final WebserviceInfo webserviceInfo) {
this.lmsSetup = lmsSetup; this.apiTemplateDataSupplier = apiTemplateDataSupplier;
this.credentials = credentials;
this.webserviceInfo = webserviceInfo; this.webserviceInfo = webserviceInfo;
this.mockups = new ArrayList<>();
final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup();
final Long lmsSetupId = lmsSetup.id; final Long lmsSetupId = lmsSetup.id;
final Long institutionId = lmsSetup.getInstitutionId(); final Long institutionId = lmsSetup.getInstitutionId();
final LmsType lmsType = lmsSetup.getLmsType(); final LmsType lmsType = lmsSetup.getLmsType();
this.mockups = new ArrayList<>();
this.mockups.add(new QuizData( this.mockups.add(new QuizData(
"quiz1", institutionId, lmsSetupId, lmsType, "Demo Quiz 1 (MOCKUP)", "<p>Demo Quiz Mockup</p>", "quiz1", institutionId, lmsSetupId, lmsType, "Demo Quiz 1 (MOCKUP)", "<p>Demo Quiz Mockup</p>",
"2020-01-01T09:00:00Z", null, "http://lms.mockup.com/api/")); "2020-01-01T09:00:00Z", null, "http://lms.mockup.com/api/"));
@ -92,24 +93,31 @@ final class MockupLmsAPITemplate implements LmsAPITemplate {
"http://lms.mockup.com/api/")); "http://lms.mockup.com/api/"));
} }
@Override
public LmsType getType() {
return LmsType.MOCKUP;
}
@Override @Override
public LmsSetup lmsSetup() { public LmsSetup lmsSetup() {
return this.lmsSetup; return this.apiTemplateDataSupplier.getLmsSetup();
} }
private List<APIMessage> checkAttributes() { private List<APIMessage> checkAttributes() {
final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup();
final ClientCredentials lmsClientCredentials = this.apiTemplateDataSupplier.getLmsClientCredentials();
final List<APIMessage> missingAttrs = new ArrayList<>(); final List<APIMessage> missingAttrs = new ArrayList<>();
if (StringUtils.isBlank(this.lmsSetup.lmsApiUrl)) { if (StringUtils.isBlank(lmsSetup.lmsApiUrl)) {
missingAttrs.add(APIMessage.fieldValidationError( missingAttrs.add(APIMessage.fieldValidationError(
LMS_SETUP.ATTR_LMS_URL, LMS_SETUP.ATTR_LMS_URL,
"lmsSetup:lmsUrl:notNull")); "lmsSetup:lmsUrl:notNull"));
} }
if (!this.credentials.hasClientId()) { if (!lmsClientCredentials.hasClientId()) {
missingAttrs.add(APIMessage.fieldValidationError( missingAttrs.add(APIMessage.fieldValidationError(
LMS_SETUP.ATTR_LMS_CLIENTNAME, LMS_SETUP.ATTR_LMS_CLIENTNAME,
"lmsSetup:lmsClientname:notNull")); "lmsSetup:lmsClientname:notNull"));
} }
if (!this.credentials.hasSecret()) { if (!lmsClientCredentials.hasSecret()) {
missingAttrs.add(APIMessage.fieldValidationError( missingAttrs.add(APIMessage.fieldValidationError(
LMS_SETUP.ATTR_LMS_CLIENTSECRET, LMS_SETUP.ATTR_LMS_CLIENTSECRET,
"lmsSetup:lmsClientsecret:notNull")); "lmsSetup:lmsClientsecret:notNull"));
@ -119,7 +127,7 @@ final class MockupLmsAPITemplate implements LmsAPITemplate {
@Override @Override
public LmsSetupTestResult testCourseAccessAPI() { public LmsSetupTestResult testCourseAccessAPI() {
log.info("Test Lms Binding for Mockup and LmsSetup: {}", this.lmsSetup); log.info("Test Lms Binding for Mockup and LmsSetup: {}", this.apiTemplateDataSupplier.getLmsSetup());
final List<APIMessage> missingAttrs = checkAttributes(); final List<APIMessage> missingAttrs = checkAttributes();
@ -136,7 +144,6 @@ final class MockupLmsAPITemplate implements LmsAPITemplate {
@Override @Override
public LmsSetupTestResult testCourseRestrictionAPI() { public LmsSetupTestResult testCourseRestrictionAPI() {
// TODO Auto-generated method stub
return LmsSetupTestResult.ofQuizRestrictionAPIError("unsupported"); return LmsSetupTestResult.ofQuizRestrictionAPIError("unsupported");
} }
@ -239,7 +246,7 @@ final class MockupLmsAPITemplate implements LmsAPITemplate {
private boolean authenticate() { private boolean authenticate() {
try { try {
final CharSequence plainClientId = this.credentials.clientId; final CharSequence plainClientId = this.apiTemplateDataSupplier.getLmsClientCredentials().clientId;
if (plainClientId == null || plainClientId.length() <= 0) { if (plainClientId == null || plainClientId.length() <= 0) {
throw new IllegalAccessException("Wrong client credential"); throw new IllegalAccessException("Wrong client credential");
} }

View file

@ -45,7 +45,8 @@ 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.Utils; import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.CourseAccess; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.AbstractCourseAccess;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleCourseDataAsyncLoader.CourseDataShort; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleCourseDataAsyncLoader.CourseDataShort;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory.MoodleAPIRestTemplate; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory.MoodleAPIRestTemplate;
@ -63,7 +64,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestT
* background task if needed and return immediately to do not block the request. * background task if needed and return immediately to do not block the request.
* The planed Moodle integration on moodle side also defines an improved course access API. This will * The planed Moodle integration on moodle side also defines an improved course access API. This will
* possibly make this synchronous fetch strategy obsolete in the future. */ * possibly make this synchronous fetch strategy obsolete in the future. */
public class MoodleCourseAccess extends CourseAccess { public class MoodleCourseAccess extends AbstractCourseAccess {
private static final long INITIAL_WAIT_TIME = 3 * Constants.SECOND_IN_MILLIS; private static final long INITIAL_WAIT_TIME = 3 * Constants.SECOND_IN_MILLIS;
@ -86,7 +87,6 @@ public class MoodleCourseAccess extends CourseAccess {
static final String MOODLE_COURSE_API_SEARCH_PAGE_SIZE = "perpage"; static final String MOODLE_COURSE_API_SEARCH_PAGE_SIZE = "perpage";
private final JSONMapper jsonMapper; private final JSONMapper jsonMapper;
private final LmsSetup lmsSetup;
private final MoodleRestTemplateFactory moodleRestTemplateFactory; private final MoodleRestTemplateFactory moodleRestTemplateFactory;
private final MoodleCourseDataAsyncLoader moodleCourseDataAsyncLoader; private final MoodleCourseDataAsyncLoader moodleCourseDataAsyncLoader;
private final CircuitBreaker<List<QuizData>> allQuizzesRequest; private final CircuitBreaker<List<QuizData>> allQuizzesRequest;
@ -96,7 +96,6 @@ public class MoodleCourseAccess extends CourseAccess {
protected MoodleCourseAccess( protected MoodleCourseAccess(
final JSONMapper jsonMapper, final JSONMapper jsonMapper,
final LmsSetup lmsSetup,
final MoodleRestTemplateFactory moodleRestTemplateFactory, final MoodleRestTemplateFactory moodleRestTemplateFactory,
final MoodleCourseDataAsyncLoader moodleCourseDataAsyncLoader, final MoodleCourseDataAsyncLoader moodleCourseDataAsyncLoader,
final AsyncService asyncService, final AsyncService asyncService,
@ -104,7 +103,6 @@ public class MoodleCourseAccess extends CourseAccess {
super(asyncService, environment); super(asyncService, environment);
this.jsonMapper = jsonMapper; this.jsonMapper = jsonMapper;
this.lmsSetup = lmsSetup;
this.moodleCourseDataAsyncLoader = moodleCourseDataAsyncLoader; this.moodleCourseDataAsyncLoader = moodleCourseDataAsyncLoader;
this.moodleRestTemplateFactory = moodleRestTemplateFactory; this.moodleRestTemplateFactory = moodleRestTemplateFactory;
@ -136,6 +134,10 @@ public class MoodleCourseAccess extends CourseAccess {
}; };
} }
APITemplateDataSupplier getApiTemplateDataSupplier() {
return this.moodleRestTemplateFactory.apiTemplateDataSupplier;
}
@Override @Override
public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeSessionId) { public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeSessionId) {
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
@ -151,8 +153,9 @@ public class MoodleCourseAccess extends CourseAccess {
queryAttributes); queryAttributes);
if (checkAccessDeniedError(userDetailsJSON)) { if (checkAccessDeniedError(userDetailsJSON)) {
final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup();
log.error("Get access denied error from Moodle: {} for API call: {}, response: {}", log.error("Get access denied error from Moodle: {} for API call: {}, response: {}",
this.lmsSetup, lmsSetup,
MOODLE_USER_PROFILE_API_FUNCTION_NAME, MOODLE_USER_PROFILE_API_FUNCTION_NAME,
Utils.truncateText(userDetailsJSON, 2000)); Utils.truncateText(userDetailsJSON, 2000));
throw new RuntimeException("No user details on Moodle API request (access-denied)"); throw new RuntimeException("No user details on Moodle API request (access-denied)");
@ -257,9 +260,11 @@ public class MoodleCourseAccess extends CourseAccess {
final MoodleAPIRestTemplate restTemplate, final MoodleAPIRestTemplate restTemplate,
final FilterMap filterMap) { final FilterMap filterMap) {
final String urlPrefix = (this.lmsSetup.lmsApiUrl.endsWith(Constants.URL_PATH_SEPARATOR)) final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup();
? this.lmsSetup.lmsApiUrl + MOODLE_QUIZ_START_URL_PATH
: this.lmsSetup.lmsApiUrl + Constants.URL_PATH_SEPARATOR + MOODLE_QUIZ_START_URL_PATH; final String urlPrefix = (lmsSetup.lmsApiUrl.endsWith(Constants.URL_PATH_SEPARATOR))
? lmsSetup.lmsApiUrl + MOODLE_QUIZ_START_URL_PATH
: lmsSetup.lmsApiUrl + Constants.URL_PATH_SEPARATOR + MOODLE_QUIZ_START_URL_PATH;
final DateTime quizFromTime = (filterMap != null) ? filterMap.getQuizFromTime() : null; final DateTime quizFromTime = (filterMap != null) ? filterMap.getQuizFromTime() : null;
final long fromCutTime = (quizFromTime != null) ? Utils.toUnixTimeInSeconds(quizFromTime) : -1; final long fromCutTime = (quizFromTime != null) ? Utils.toUnixTimeInSeconds(quizFromTime) : -1;
@ -313,10 +318,10 @@ public class MoodleCourseAccess extends CourseAccess {
private List<QuizData> getCached() { private List<QuizData> getCached() {
final Collection<CourseDataShort> courseQuizData = this.moodleCourseDataAsyncLoader.getCachedCourseData(); final Collection<CourseDataShort> courseQuizData = this.moodleCourseDataAsyncLoader.getCachedCourseData();
final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup();
final String urlPrefix = (this.lmsSetup.lmsApiUrl.endsWith(Constants.URL_PATH_SEPARATOR)) final String urlPrefix = (lmsSetup.lmsApiUrl.endsWith(Constants.URL_PATH_SEPARATOR))
? this.lmsSetup.lmsApiUrl + MOODLE_QUIZ_START_URL_PATH ? lmsSetup.lmsApiUrl + MOODLE_QUIZ_START_URL_PATH
: this.lmsSetup.lmsApiUrl + Constants.URL_PATH_SEPARATOR + MOODLE_QUIZ_START_URL_PATH; : lmsSetup.lmsApiUrl + Constants.URL_PATH_SEPARATOR + MOODLE_QUIZ_START_URL_PATH;
return reduceCoursesToQuizzes(urlPrefix, courseQuizData); return reduceCoursesToQuizzes(urlPrefix, courseQuizData);
} }
@ -325,13 +330,14 @@ public class MoodleCourseAccess extends CourseAccess {
final String urlPrefix, final String urlPrefix,
final Collection<CourseDataShort> courseQuizData) { final Collection<CourseDataShort> courseQuizData) {
final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup();
return courseQuizData return courseQuizData
.stream() .stream()
.reduce( .reduce(
new ArrayList<>(), new ArrayList<>(),
(list, courseData) -> { (list, courseData) -> {
list.addAll(quizDataOf( list.addAll(quizDataOf(
this.lmsSetup, lmsSetup,
courseData, courseData,
urlPrefix)); urlPrefix));
return list; return list;
@ -380,16 +386,17 @@ public class MoodleCourseAccess extends CourseAccess {
final CourseQuizData courseQuizData = this.jsonMapper.readValue( final CourseQuizData courseQuizData = this.jsonMapper.readValue(
quizzesJSON, quizzesJSON,
CourseQuizData.class); CourseQuizData.class);
final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup();
if (courseQuizData == null) { if (courseQuizData == null) {
log.error("No quizzes found for ids: {} on LMS; {}", quizIds, this.lmsSetup.name); log.error("No quizzes found for ids: {} on LMS; {}", quizIds, lmsSetup.name);
return Collections.emptyList(); return Collections.emptyList();
} }
logMoodleWarnings(courseQuizData.warnings); logMoodleWarnings(courseQuizData.warnings);
if (courseQuizData.quizzes == null || courseQuizData.quizzes.isEmpty()) { if (courseQuizData.quizzes == null || courseQuizData.quizzes.isEmpty()) {
log.error("No quizzes found for ids: {} on LMS; {}", quizIds, this.lmsSetup.name); log.error("No quizzes found for ids: {} on LMS; {}", quizIds, lmsSetup.name);
return Collections.emptyList(); return Collections.emptyList();
} }
@ -402,9 +409,9 @@ public class MoodleCourseAccess extends CourseAccess {
} }
}); });
final String urlPrefix = (this.lmsSetup.lmsApiUrl.endsWith(Constants.URL_PATH_SEPARATOR)) final String urlPrefix = (lmsSetup.lmsApiUrl.endsWith(Constants.URL_PATH_SEPARATOR))
? this.lmsSetup.lmsApiUrl + MOODLE_QUIZ_START_URL_PATH ? lmsSetup.lmsApiUrl + MOODLE_QUIZ_START_URL_PATH
: this.lmsSetup.lmsApiUrl + Constants.URL_PATH_SEPARATOR + MOODLE_QUIZ_START_URL_PATH; : lmsSetup.lmsApiUrl + Constants.URL_PATH_SEPARATOR + MOODLE_QUIZ_START_URL_PATH;
return courseData.values() return courseData.values()
.stream() .stream()
@ -413,7 +420,7 @@ public class MoodleCourseAccess extends CourseAccess {
new ArrayList<>(), new ArrayList<>(),
(list, cd) -> { (list, cd) -> {
list.addAll(quizDataOf( list.addAll(quizDataOf(
this.lmsSetup, lmsSetup,
cd, cd,
urlPrefix)); urlPrefix));
return list; return list;
@ -451,16 +458,17 @@ public class MoodleCourseAccess extends CourseAccess {
final Courses courses = this.jsonMapper.readValue( final Courses courses = this.jsonMapper.readValue(
coursePageJSON, coursePageJSON,
Courses.class); Courses.class);
final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup();
if (courses == null) { if (courses == null) {
log.error("No courses found for ids: {} on LMS: {}", ids, this.lmsSetup.name); log.error("No courses found for ids: {} on LMS: {}", ids, lmsSetup.name);
return Collections.emptyList(); return Collections.emptyList();
} }
logMoodleWarnings(courses.warnings); logMoodleWarnings(courses.warnings);
if (courses.courses == null || courses.courses.isEmpty()) { if (courses.courses == null || courses.courses.isEmpty()) {
log.error("No courses found for ids: {} on LMS: {}", ids, this.lmsSetup.name); log.error("No courses found for ids: {} on LMS: {}", ids, lmsSetup.name);
return Collections.emptyList(); return Collections.emptyList();
} }
@ -630,9 +638,10 @@ public class MoodleCourseAccess extends CourseAccess {
private void logMoodleWarnings(final Collection<Warning> warnings) { private void logMoodleWarnings(final Collection<Warning> warnings) {
if (warnings != null && !warnings.isEmpty()) { if (warnings != null && !warnings.isEmpty()) {
if (log.isDebugEnabled()) { if (log.isDebugEnabled()) {
final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup();
log.debug( log.debug(
"There are warnings from Moodle response: Moodle: {} request: {} warnings: {} warning sample: {}", "There are warnings from Moodle response: Moodle: {} request: {} warnings: {} warning sample: {}",
this.lmsSetup, lmsSetup,
MoodleCourseAccess.MOODLE_QUIZ_API_FUNCTION_NAME, MoodleCourseAccess.MOODLE_QUIZ_API_FUNCTION_NAME,
warnings.size(), warnings.size(),
warnings.iterator().next().toString()); warnings.iterator().next().toString());

View file

@ -24,6 +24,7 @@ import ch.ethz.seb.sebserver.gbl.model.exam.MoodleSEBRestriction;
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.exam.SEBRestriction; import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction;
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;
@ -51,23 +52,27 @@ public class MoodleLmsAPITemplate implements LmsAPITemplate {
private static final Logger log = LoggerFactory.getLogger(MoodleLmsAPITemplate.class); private static final Logger log = LoggerFactory.getLogger(MoodleLmsAPITemplate.class);
private final LmsSetup lmsSetup;
private final MoodleCourseAccess moodleCourseAccess; private final MoodleCourseAccess moodleCourseAccess;
private final MoodleCourseRestriction moodleCourseRestriction; private final MoodleCourseRestriction moodleCourseRestriction;
protected MoodleLmsAPITemplate( protected MoodleLmsAPITemplate(
final LmsSetup lmsSetup,
final MoodleCourseAccess moodleCourseAccess, final MoodleCourseAccess moodleCourseAccess,
final MoodleCourseRestriction moodleCourseRestriction) { final MoodleCourseRestriction moodleCourseRestriction) {
this.lmsSetup = lmsSetup;
this.moodleCourseAccess = moodleCourseAccess; this.moodleCourseAccess = moodleCourseAccess;
this.moodleCourseRestriction = moodleCourseRestriction; this.moodleCourseRestriction = moodleCourseRestriction;
} }
@Override
public LmsType getType() {
return LmsType.MOODLE;
}
@Override @Override
public LmsSetup lmsSetup() { public LmsSetup lmsSetup() {
return this.lmsSetup; return this.moodleCourseAccess
.getApiTemplateDataSupplier()
.getLmsSetup();
} }
@Override @Override

View file

@ -20,12 +20,11 @@ import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.JSONMapper; import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
import ch.ethz.seb.sebserver.gbl.async.AsyncService; import ch.ethz.seb.sebserver.gbl.async.AsyncService;
import ch.ethz.seb.sebserver.gbl.client.ClientCredentialService; import ch.ethz.seb.sebserver.gbl.client.ClientCredentialService;
import ch.ethz.seb.sebserver.gbl.client.ClientCredentials;
import ch.ethz.seb.sebserver.gbl.client.ProxyData;
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.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.lms.APITemplateDataSupplier;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplateFactory; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplateFactory;
@ -68,29 +67,25 @@ public class MoodleLmsAPITemplateFactory implements LmsAPITemplateFactory {
} }
@Override @Override
public Result<LmsAPITemplate> create( public Result<LmsAPITemplate> create(final APITemplateDataSupplier apiTemplateDataSupplier) {
final LmsSetup lmsSetup,
final ClientCredentials credentials,
final ProxyData proxyData) {
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
final MoodleCourseDataAsyncLoader asyncLoaderPrototype = final LmsSetup lmsSetup = apiTemplateDataSupplier.getLmsSetup();
this.applicationContext.getBean(MoodleCourseDataAsyncLoader.class);
asyncLoaderPrototype.init(lmsSetup.name); final MoodleCourseDataAsyncLoader asyncLoaderPrototype = this.applicationContext
.getBean(MoodleCourseDataAsyncLoader.class);
asyncLoaderPrototype.init(lmsSetup.getModelId());
final MoodleRestTemplateFactory moodleRestTemplateFactory = new MoodleRestTemplateFactory( final MoodleRestTemplateFactory moodleRestTemplateFactory = new MoodleRestTemplateFactory(
this.jsonMapper, this.jsonMapper,
lmsSetup, apiTemplateDataSupplier,
credentials,
proxyData,
this.clientCredentialService, this.clientCredentialService,
this.clientHttpRequestFactoryService, this.clientHttpRequestFactoryService,
this.alternativeTokenRequestPaths); this.alternativeTokenRequestPaths);
final MoodleCourseAccess moodleCourseAccess = new MoodleCourseAccess( final MoodleCourseAccess moodleCourseAccess = new MoodleCourseAccess(
this.jsonMapper, this.jsonMapper,
lmsSetup,
moodleRestTemplateFactory, moodleRestTemplateFactory,
asyncLoaderPrototype, asyncLoaderPrototype,
this.asyncService, this.asyncService,
@ -101,7 +96,6 @@ public class MoodleLmsAPITemplateFactory implements LmsAPITemplateFactory {
moodleRestTemplateFactory); moodleRestTemplateFactory);
return new MoodleLmsAPITemplate( return new MoodleLmsAPITemplate(
lmsSetup,
moodleCourseAccess, moodleCourseAccess,
moodleCourseRestriction); moodleCourseRestriction);
}); });

View file

@ -50,33 +50,28 @@ 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.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.lms.APITemplateDataSupplier;
class MoodleRestTemplateFactory { class MoodleRestTemplateFactory {
private static final Logger log = LoggerFactory.getLogger(MoodleRestTemplateFactory.class); private static final Logger log = LoggerFactory.getLogger(MoodleRestTemplateFactory.class);
final JSONMapper jsonMapper; final JSONMapper jsonMapper;
final LmsSetup lmsSetup; final APITemplateDataSupplier apiTemplateDataSupplier;
final ClientCredentials credentials;
final ProxyData proxyData;
final ClientHttpRequestFactoryService clientHttpRequestFactoryService; final ClientHttpRequestFactoryService clientHttpRequestFactoryService;
final ClientCredentialService clientCredentialService; final ClientCredentialService clientCredentialService;
final Set<String> knownTokenAccessPaths; final Set<String> knownTokenAccessPaths;
public MoodleRestTemplateFactory( public MoodleRestTemplateFactory(
final JSONMapper jsonMapper, final JSONMapper jsonMapper,
final LmsSetup lmsSetup, final APITemplateDataSupplier apiTemplateDataSupplier,
final ClientCredentials credentials,
final ProxyData proxyData,
final ClientCredentialService clientCredentialService, final ClientCredentialService clientCredentialService,
final ClientHttpRequestFactoryService clientHttpRequestFactoryService, final ClientHttpRequestFactoryService clientHttpRequestFactoryService,
final String[] alternativeTokenRequestPaths) { final String[] alternativeTokenRequestPaths) {
this.jsonMapper = jsonMapper; this.jsonMapper = jsonMapper;
this.lmsSetup = lmsSetup; this.apiTemplateDataSupplier = apiTemplateDataSupplier;
this.clientCredentialService = clientCredentialService; this.clientCredentialService = clientCredentialService;
this.credentials = credentials;
this.proxyData = proxyData;
this.clientHttpRequestFactoryService = clientHttpRequestFactoryService; this.clientHttpRequestFactoryService = clientHttpRequestFactoryService;
this.knownTokenAccessPaths = new HashSet<>(); this.knownTokenAccessPaths = new HashSet<>();
@ -86,28 +81,36 @@ class MoodleRestTemplateFactory {
} }
} }
APITemplateDataSupplier getApiTemplateDataSupplier() {
return this.apiTemplateDataSupplier;
}
public LmsSetupTestResult test() { public LmsSetupTestResult test() {
final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup();
final ClientCredentials credentials = this.apiTemplateDataSupplier.getLmsClientCredentials();
final List<APIMessage> missingAttrs = new ArrayList<>(); final List<APIMessage> missingAttrs = new ArrayList<>();
if (StringUtils.isBlank(this.lmsSetup.lmsApiUrl)) { if (StringUtils.isBlank(lmsSetup.lmsApiUrl)) {
missingAttrs.add(APIMessage.fieldValidationError( missingAttrs.add(APIMessage.fieldValidationError(
LMS_SETUP.ATTR_LMS_URL, LMS_SETUP.ATTR_LMS_URL,
"lmsSetup:lmsUrl:notNull")); "lmsSetup:lmsUrl:notNull"));
} else { } else {
// try to connect to the url // try to connect to the url
if (!Utils.pingHost(this.lmsSetup.lmsApiUrl)) { if (!Utils.pingHost(lmsSetup.lmsApiUrl)) {
missingAttrs.add(APIMessage.fieldValidationError( missingAttrs.add(APIMessage.fieldValidationError(
LMS_SETUP.ATTR_LMS_URL, LMS_SETUP.ATTR_LMS_URL,
"lmsSetup:lmsUrl:url.invalid")); "lmsSetup:lmsUrl:url.invalid"));
} }
} }
if (StringUtils.isBlank(this.lmsSetup.lmsRestApiToken)) { if (StringUtils.isBlank(lmsSetup.lmsRestApiToken)) {
if (!this.credentials.hasClientId()) { if (!credentials.hasClientId()) {
missingAttrs.add(APIMessage.fieldValidationError( missingAttrs.add(APIMessage.fieldValidationError(
LMS_SETUP.ATTR_LMS_CLIENTNAME, LMS_SETUP.ATTR_LMS_CLIENTNAME,
"lmsSetup:lmsClientname:notNull")); "lmsSetup:lmsClientname:notNull"));
} }
if (!this.credentials.hasSecret()) { if (!credentials.hasSecret()) {
missingAttrs.add(APIMessage.fieldValidationError( missingAttrs.add(APIMessage.fieldValidationError(
LMS_SETUP.ATTR_LMS_CLIENTSECRET, LMS_SETUP.ATTR_LMS_CLIENTSECRET,
"lmsSetup:lmsClientsecret:notNull")); "lmsSetup:lmsClientsecret:notNull"));
@ -122,14 +125,17 @@ class MoodleRestTemplateFactory {
} }
Result<MoodleAPIRestTemplate> createRestTemplate() { Result<MoodleAPIRestTemplate> createRestTemplate() {
final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup();
return this.knownTokenAccessPaths return this.knownTokenAccessPaths
.stream() .stream()
.map(this::createRestTemplate) .map(this::createRestTemplate)
.map(result -> { .map(result -> {
if (result.hasError()) { if (result.hasError()) {
log.warn("Failed to get access token for LMS: {}({})", log.warn("Failed to get access token for LMS: {}({})",
this.lmsSetup.name, lmsSetup.name,
this.lmsSetup.id, lmsSetup.id,
result.getError()); result.getError());
} }
return result; return result;
@ -138,53 +144,48 @@ class MoodleRestTemplateFactory {
.findFirst() .findFirst()
.orElse(Result.ofRuntimeError( .orElse(Result.ofRuntimeError(
"Failed to gain any access for LMS " + "Failed to gain any access for LMS " +
this.lmsSetup.name + "(" + this.lmsSetup.id + lmsSetup.name + "(" + lmsSetup.id +
") on paths: " + this.knownTokenAccessPaths)); ") on paths: " + this.knownTokenAccessPaths));
} }
Result<MoodleAPIRestTemplate> createRestTemplate(final String accessTokenPath) { Result<MoodleAPIRestTemplate> createRestTemplate(final String accessTokenPath) {
return Result.tryCatch(() -> {
final MoodleAPIRestTemplate template = createRestTemplate(
this.credentials,
accessTokenPath);
final CharSequence accessToken = template.getAccessToken(); final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup();
return Result.tryCatch(() -> {
final ClientCredentials credentials = this.apiTemplateDataSupplier.getLmsClientCredentials();
final ProxyData proxyData = this.apiTemplateDataSupplier.getProxyData();
final CharSequence plainClientId = credentials.clientId;
final CharSequence plainClientSecret = this.clientCredentialService
.getPlainClientSecret(credentials)
.getOrThrow();
final MoodleAPIRestTemplate restTemplate = new MoodleAPIRestTemplate(
this.jsonMapper,
lmsSetup.lmsApiUrl,
accessTokenPath,
lmsSetup.lmsRestApiToken,
plainClientId,
plainClientSecret);
final ClientHttpRequestFactory clientHttpRequestFactory = this.clientHttpRequestFactoryService
.getClientHttpRequestFactory(proxyData)
.getOrThrow();
restTemplate.setRequestFactory(clientHttpRequestFactory);
final CharSequence accessToken = restTemplate.getAccessToken();
if (accessToken == null) { if (accessToken == null) {
throw new RuntimeException("Failed to get access token for LMS " + throw new RuntimeException("Failed to get access token for LMS " +
this.lmsSetup.name + "(" + this.lmsSetup.id + lmsSetup.name + "(" + lmsSetup.id +
") on path: " + accessTokenPath); ") on path: " + accessTokenPath);
} }
return template; return restTemplate;
}); });
} }
protected MoodleAPIRestTemplate createRestTemplate(
final ClientCredentials credentials,
final String accessTokenRequestPath) {
final CharSequence plainClientId = credentials.clientId;
final CharSequence plainClientSecret = this.clientCredentialService
.getPlainClientSecret(credentials)
.getOrThrow();
final MoodleAPIRestTemplate restTemplate = new MoodleAPIRestTemplate(
this.jsonMapper,
this.lmsSetup.lmsApiUrl,
accessTokenRequestPath,
this.lmsSetup.lmsRestApiToken,
plainClientId,
plainClientSecret);
final ClientHttpRequestFactory clientHttpRequestFactory = this.clientHttpRequestFactoryService
.getClientHttpRequestFactory(this.proxyData)
.getOrThrow();
restTemplate.setRequestFactory(clientHttpRequestFactory);
return restTemplate;
}
public class MoodleAPIRestTemplate extends RestTemplate { public class MoodleAPIRestTemplate extends RestTemplate {
public static final String URI_VAR_USER_NAME = "username"; public static final String URI_VAR_USER_NAME = "username";
@ -210,7 +211,6 @@ class MoodleRestTemplateFactory {
private final HttpEntity<?> tokenReqEntity = new HttpEntity<>(new LinkedMultiValueMap<>()); private final HttpEntity<?> tokenReqEntity = new HttpEntity<>(new LinkedMultiValueMap<>());
protected MoodleAPIRestTemplate( protected MoodleAPIRestTemplate(
final JSONMapper jsonMapper, final JSONMapper jsonMapper,
final String serverURL, final String serverURL,
final String tokenPath, final String tokenPath,
@ -318,10 +318,13 @@ class MoodleRestTemplateFactory {
functionReqEntity, functionReqEntity,
String.class); String.class);
final LmsSetup lmsSetup = MoodleRestTemplateFactory.this.apiTemplateDataSupplier
.getLmsSetup();
if (response.getStatusCode() != HttpStatus.OK) { if (response.getStatusCode() != HttpStatus.OK) {
throw new RuntimeException( throw new RuntimeException(
"Failed to call Moodle webservice API function: " + functionName + " lms setup: " + "Failed to call Moodle webservice API function: " + functionName + " lms setup: " +
MoodleRestTemplateFactory.this.lmsSetup + " response: " + response.getBody()); lmsSetup + " response: " + response.getBody());
} }
final String body = response.getBody(); final String body = response.getBody();
@ -335,7 +338,7 @@ class MoodleRestTemplateFactory {
this.accessToken = null; this.accessToken = null;
throw new RuntimeException( throw new RuntimeException(
"Failed to call Moodle webservice API function: " + functionName + " lms setup: " + "Failed to call Moodle webservice API function: " + functionName + " lms setup: " +
MoodleRestTemplateFactory.this.lmsSetup + " response: " + body); lmsSetup + " response: " + body);
} }
return body; return body;
@ -343,7 +346,11 @@ class MoodleRestTemplateFactory {
private void requestAccessToken() { private void requestAccessToken() {
final LmsSetup lmsSetup = MoodleRestTemplateFactory.this.apiTemplateDataSupplier
.getLmsSetup();
try { try {
final ResponseEntity<String> response = super.exchange( final ResponseEntity<String> response = super.exchange(
this.serverURL + this.tokenPath, this.serverURL + this.tokenPath,
HttpMethod.GET, HttpMethod.GET,
@ -353,11 +360,11 @@ class MoodleRestTemplateFactory {
if (response.getStatusCode() != HttpStatus.OK) { if (response.getStatusCode() != HttpStatus.OK) {
log.error("Failed to gain access token for LMS (Moodle): lmsSetup: {} response: {} : {}", log.error("Failed to gain access token for LMS (Moodle): lmsSetup: {} response: {} : {}",
MoodleRestTemplateFactory.this.lmsSetup, lmsSetup,
response.getStatusCode(), response.getStatusCode(),
response.getBody()); response.getBody());
throw new RuntimeException("Failed to gain access token for LMS (Moodle): lmsSetup: " + throw new RuntimeException("Failed to gain access token for LMS (Moodle): lmsSetup: " +
MoodleRestTemplateFactory.this.lmsSetup + " response: " + response.getBody()); lmsSetup + " response: " + response.getBody());
} }
try { try {
@ -369,25 +376,25 @@ class MoodleRestTemplateFactory {
throw new RuntimeException("Access Token request with 200 but no or invalid token body"); throw new RuntimeException("Access Token request with 200 but no or invalid token body");
} else { } else {
log.info("Successfully get access token from Moodle: {}", log.info("Successfully get access token from Moodle: {}",
MoodleRestTemplateFactory.this.lmsSetup); lmsSetup);
} }
this.accessToken = moodleToken.token; this.accessToken = moodleToken.token;
} catch (final Exception e) { } catch (final Exception e) {
log.error("Failed to gain access token for LMS (Moodle): lmsSetup: {} response: {} : {}", log.error("Failed to gain access token for LMS (Moodle): lmsSetup: {} response: {} : {}",
MoodleRestTemplateFactory.this.lmsSetup, lmsSetup,
response.getStatusCode(), response.getStatusCode(),
response.getBody()); response.getBody());
throw new RuntimeException("Failed to gain access token for LMS (Moodle): lmsSetup: " + throw new RuntimeException("Failed to gain access token for LMS (Moodle): lmsSetup: " +
MoodleRestTemplateFactory.this.lmsSetup + " response: " + response.getBody(), e); lmsSetup + " response: " + response.getBody(), e);
} }
} catch (final Exception e) { } catch (final Exception e) {
log.error("Failed to gain access token for LMS (Moodle): lmsSetup: {} :", log.error("Failed to gain access token for LMS (Moodle): lmsSetup: {} :",
MoodleRestTemplateFactory.this.lmsSetup, lmsSetup,
e); e);
throw new RuntimeException("Failed to gain access token for LMS (Moodle): lmsSetup: " + throw new RuntimeException("Failed to gain access token for LMS (Moodle): lmsSetup: " +
MoodleRestTemplateFactory.this.lmsSetup + " cause: " + e.getMessage()); lmsSetup + " cause: " + e.getMessage());
} }
} }

View file

@ -34,8 +34,6 @@ import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.api.APIMessage.ErrorMessage; import ch.ethz.seb.sebserver.gbl.api.APIMessage.ErrorMessage;
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.Exam.ExamStatus; import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamStatus;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.Features;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
@ -49,7 +47,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.IndicatorDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.IndicatorDAO;
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.impl.NoSEBRestrictionException; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.indicator.IndicatorDistributedRequestCache; import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.indicator.IndicatorDistributedRequestCache;
@ -67,7 +65,7 @@ public class ExamSessionServiceImpl implements ExamSessionService {
private final ExamDAO examDAO; private final ExamDAO examDAO;
private final ExamConfigurationMapDAO examConfigurationMapDAO; private final ExamConfigurationMapDAO examConfigurationMapDAO;
private final CacheManager cacheManager; private final CacheManager cacheManager;
private final LmsAPIService lmsAPIService; private final SEBRestrictionService sebRestrictionService;
private final IndicatorDistributedRequestCache indicatorDistributedRequestCache; private final IndicatorDistributedRequestCache indicatorDistributedRequestCache;
private final boolean distributedSetup; private final boolean distributedSetup;
@ -79,7 +77,7 @@ public class ExamSessionServiceImpl implements ExamSessionService {
final ClientConnectionDAO clientConnectionDAO, final ClientConnectionDAO clientConnectionDAO,
final IndicatorDAO indicatorDAO, final IndicatorDAO indicatorDAO,
final CacheManager cacheManager, final CacheManager cacheManager,
final LmsAPIService lmsAPIService, final SEBRestrictionService sebRestrictionService,
final IndicatorDistributedRequestCache indicatorDistributedRequestCache, final IndicatorDistributedRequestCache indicatorDistributedRequestCache,
@Value("${sebserver.webservice.distributed:false}") final boolean distributedSetup) { @Value("${sebserver.webservice.distributed:false}") final boolean distributedSetup) {
@ -90,7 +88,7 @@ public class ExamSessionServiceImpl implements ExamSessionService {
this.clientConnectionDAO = clientConnectionDAO; this.clientConnectionDAO = clientConnectionDAO;
this.cacheManager = cacheManager; this.cacheManager = cacheManager;
this.indicatorDAO = indicatorDAO; this.indicatorDAO = indicatorDAO;
this.lmsAPIService = lmsAPIService; this.sebRestrictionService = sebRestrictionService;
this.indicatorDistributedRequestCache = indicatorDistributedRequestCache; this.indicatorDistributedRequestCache = indicatorDistributedRequestCache;
this.distributedSetup = distributedSetup; this.distributedSetup = distributedSetup;
} }
@ -117,7 +115,7 @@ public class ExamSessionServiceImpl implements ExamSessionService {
@Override @Override
public LmsAPIService getLmsAPIService() { public LmsAPIService getLmsAPIService() {
return this.lmsAPIService; return this.sebRestrictionService.getLmsAPIService();
} }
@Override @Override
@ -149,29 +147,10 @@ public class ExamSessionServiceImpl implements ExamSessionService {
return null; return null;
}); });
// check SEB restriction available and restricted if (!this.sebRestrictionService.checkConsistency(exam.lmsSetupId, exam)) {
// if SEB restriction is not available no consistency violation message is added result.add(
final LmsSetup lmsSetup = this.lmsAPIService.getLmsSetup(exam.lmsSetupId) ErrorMessage.EXAM_CONSISTENCY_VALIDATION_SEB_RESTRICTION
.getOr(null); .of(exam.getModelId()));
if (lmsSetup != null && lmsSetup.lmsType.features.contains(Features.SEB_RESTRICTION)) {
this.lmsAPIService.getLmsAPITemplate(exam.lmsSetupId)
.map(t -> {
if (t.testCourseRestrictionAPI().isOk()) {
return t;
} else {
throw new NoSEBRestrictionException();
}
})
.flatMap(t -> t.getSEBClientRestriction(exam))
.onError(error -> {
if (error instanceof NoSEBRestrictionException) {
result.add(
ErrorMessage.EXAM_CONSISTENCY_VALIDATION_SEB_RESTRICTION
.of(exam.getModelId()));
} else {
throw new RuntimeException("Unexpected error: ", error);
}
});
} }
// check indicator exists // check indicator exists

View file

@ -246,12 +246,21 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
@RequestParam( @RequestParam(
name = API.PARAM_INSTITUTION_ID, name = API.PARAM_INSTITUTION_ID,
required = true, required = true,
defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId) { defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId,
@RequestParam(
name = API.EXAM_ADMINISTRATION_CONSISTENCY_CHECK_INCLUDE_RESTRICTION,
defaultValue = "false") final boolean includeRestriction) {
checkReadPrivilege(institutionId); checkReadPrivilege(institutionId);
return this.examSessionService final Collection<APIMessage> result = this.examSessionService
.checkExamConsistency(modelId) .checkExamConsistency(modelId)
.getOrThrow(); .getOrThrow();
if (includeRestriction) {
// TODO include seb restriction check and status
}
return result;
} }
// **************************************************************************** // ****************************************************************************
@ -524,37 +533,43 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
} }
private Result<Exam> applySEBRestriction(final Exam exam, final boolean restrict) { private Result<Exam> applySEBRestriction(final Exam exam, final boolean restrict) {
final LmsSetup lmsSetup = this.lmsAPIService.getLmsSetup(exam.lmsSetupId)
.getOrThrow();
if (!lmsSetup.lmsType.features.contains(Features.SEB_RESTRICTION)) { return Result.tryCatch(() -> {
return Result.ofError(new UnsupportedOperationException( final LmsSetup lmsSetup = this.lmsAPIService.getLmsSetup(exam.lmsSetupId)
"SEB Restriction feature not available for LMS type: " + lmsSetup.lmsType)); .getOrThrow();
}
if (restrict) { if (!lmsSetup.lmsType.features.contains(Features.SEB_RESTRICTION)) {
if (!this.lmsAPIService throw new UnsupportedOperationException(
.getLmsSetup(exam.lmsSetupId) "SEB Restriction feature not available for LMS type: " + lmsSetup.lmsType);
.getOrThrow().lmsType.features.contains(Features.SEB_RESTRICTION)) {
return Result.ofError(new APIMessageException(
APIMessage.ErrorMessage.ILLEGAL_API_ARGUMENT
.of("The LMS for this Exam has no SEB restriction feature")));
} }
if (this.examSessionService.hasActiveSEBClientConnections(exam.id)) { if (restrict) {
return Result.ofError(new APIMessageException( if (!this.lmsAPIService
APIMessage.ErrorMessage.INTEGRITY_VALIDATION .getLmsSetup(exam.lmsSetupId)
.of("Exam currently has active SEB Client connections."))); .getOrThrow().lmsType.features.contains(Features.SEB_RESTRICTION)) {
}
return this.checkNoActiveSEBClientConnections(exam) throw new APIMessageException(
.flatMap(this.sebRestrictionService::applySEBClientRestriction) APIMessage.ErrorMessage.ILLEGAL_API_ARGUMENT
.flatMap(e -> this.examDAO.setSEBRestriction(exam.id, restrict)); .of("The LMS for this Exam has no SEB restriction feature"));
} else { }
return this.sebRestrictionService.releaseSEBClientRestriction(exam)
.flatMap(e -> this.examDAO.setSEBRestriction(exam.id, restrict)); if (this.examSessionService.hasActiveSEBClientConnections(exam.id)) {
} throw new APIMessageException(
APIMessage.ErrorMessage.INTEGRITY_VALIDATION
.of("Exam currently has active SEB Client connections."));
}
// TODO double check before setSEBRestriction
return this.checkNoActiveSEBClientConnections(exam)
.flatMap(this.sebRestrictionService::applySEBClientRestriction)
.flatMap(e -> this.examDAO.setSEBRestriction(exam.id, restrict))
.getOrThrow();
} else {
return this.sebRestrictionService.releaseSEBClientRestriction(exam)
.flatMap(e -> this.examDAO.setSEBRestriction(exam.id, restrict))
.getOrThrow();
}
});
} }
static Function<Collection<Exam>, List<Exam>> pageSort(final String sort) { static Function<Collection<Exam>, List<Exam>> pageSort(final String sort) {

View file

@ -30,7 +30,6 @@ import ch.ethz.seb.sebserver.gbl.model.Entity;
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.profile.WebServiceProfile; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.LmsSetupRecordDynamicSqlSupport; import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.LmsSetupRecordDynamicSqlSupport;
import ch.ethz.seb.sebserver.webservice.servicelayer.PaginationService; import ch.ethz.seb.sebserver.webservice.servicelayer.PaginationService;
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.AuthorizationService; import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.AuthorizationService;
@ -39,7 +38,6 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkActionServic
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.dao.UserActivityLogDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO;
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.impl.LmsSetupChangeEvent;
import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationService; import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationService;
@WebServiceProfile @WebServiceProfile
@ -134,10 +132,4 @@ public class LmsSetupController extends ActivatableEntityController<LmsSetup, Lm
return new LmsSetup(null, postParams); return new LmsSetup(null, postParams);
} }
@Override
protected Result<LmsSetup> notifySaved(final LmsSetup entity) {
this.applicationEventPublisher.publishEvent(new LmsSetupChangeEvent(entity));
return super.notifySaved(entity);
}
} }

View file

@ -193,7 +193,7 @@ public class ModelObjectJSONGenerator {
1L, 1L, 1L, "externalId", "name", "description", DateTime.now(), DateTime.now(), 1L, 1L, 1L, "externalId", "name", "description", DateTime.now(), DateTime.now(),
"startURL", ExamType.BYOD, "owner", "startURL", ExamType.BYOD, "owner",
Arrays.asList("user1", "user2"), Arrays.asList("user1", "user2"),
ExamStatus.RUNNING, "browserExamKeys", true, null); ExamStatus.RUNNING, false, "browserExamKeys", true, null);
System.out.println(domainObject.getClass().getSimpleName() + ":"); System.out.println(domainObject.getClass().getSimpleName() + ":");
System.out.println(writerWithDefaultPrettyPrinter.writeValueAsString(domainObject)); System.out.println(writerWithDefaultPrettyPrinter.writeValueAsString(domainObject));

View file

@ -850,6 +850,7 @@ public class UseCasesIntegrationTest extends GuiIntegrationTest {
null, null,
Utils.immutableCollectionOf(userId), Utils.immutableCollectionOf(userId),
ExamStatus.RUNNING, ExamStatus.RUNNING,
false,
null, null,
true, true,
null); null);

View file

@ -64,6 +64,7 @@ public class ExamAPITest extends AdministrationAPIIntegrationTester {
exam.owner, exam.owner,
Arrays.asList("user5"), Arrays.asList("user5"),
null, null,
false,
null, null,
true, true,
null)) null))
@ -94,6 +95,7 @@ public class ExamAPITest extends AdministrationAPIIntegrationTester {
exam.owner, exam.owner,
Arrays.asList("user2"), Arrays.asList("user2"),
null, null,
false,
null, null,
true, true,
null)) null))

View file

@ -10,7 +10,10 @@ package ch.ethz.seb.sebserver.webservice.integration.api.admin;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import org.junit.After;
import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.test.context.jdbc.Sql; import org.springframework.test.context.jdbc.Sql;
@ -23,10 +26,25 @@ import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamType; import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamType;
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.webservice.servicelayer.lms.LmsAPIService;
@Sql(scripts = { "classpath:schema-test.sql", "classpath:data-test.sql" })
public class ExamImportTest extends AdministrationAPIIntegrationTester { public class ExamImportTest extends AdministrationAPIIntegrationTester {
@Autowired
private LmsAPIService lmsAPIService;
@Before
@Sql(scripts = { "classpath:schema-test.sql", "classpath:data-test.sql" })
public void init() {
this.lmsAPIService.cleanup();
}
@After
@Sql(scripts = { "classpath:schema-test.sql", "classpath:data-test.sql" })
public void cleanup() {
this.lmsAPIService.cleanup();
}
@Test @Test
public void testImportFromQuiz() throws Exception { public void testImportFromQuiz() throws Exception {
// create new active LmsSetup Mock with seb-admin // create new active LmsSetup Mock with seb-admin
@ -57,7 +75,7 @@ public class ExamImportTest extends AdministrationAPIIntegrationTester {
this, this,
getSebAdminAccess(), getSebAdminAccess(),
getSebAdminAccess(), getSebAdminAccess(),
"LmsSetupMock", "LmsSetupMock1",
"quiz2", "quiz2",
ExamType.MANAGED, ExamType.MANAGED,
"user5"); "user5");
@ -76,7 +94,7 @@ public class ExamImportTest extends AdministrationAPIIntegrationTester {
this, this,
getAdminInstitution2Access(), getAdminInstitution2Access(),
getAdminInstitution2Access(), getAdminInstitution2Access(),
"LmsSetupMock", "LmsSetupMock2",
"quiz2", "quiz2",
ExamType.MANAGED, ExamType.MANAGED,
"user7"); "user7");
@ -90,7 +108,7 @@ public class ExamImportTest extends AdministrationAPIIntegrationTester {
this, this,
getAdminInstitution2Access(), getAdminInstitution2Access(),
getExamAdmin1(), // this exam administrator is on Institution 2 getExamAdmin1(), // this exam administrator is on Institution 2
"LmsSetupMock2", "LmsSetupMock3",
"quiz2", "quiz2",
ExamType.MANAGED, ExamType.MANAGED,
"user7"); "user7");
@ -108,7 +126,7 @@ public class ExamImportTest extends AdministrationAPIIntegrationTester {
this, this,
getAdminInstitution1Access(), getAdminInstitution1Access(),
getExamAdmin1(), // this exam administrator is on Institution 2 getExamAdmin1(), // this exam administrator is on Institution 2
"LmsSetupMock", "LmsSetupMock4",
"quiz2", "quiz2",
ExamType.MANAGED, ExamType.MANAGED,
"user7"); "user7");

View file

@ -71,7 +71,6 @@ public class MoodleCourseAccessTest {
final MoodleCourseAccess moodleCourseAccess = new MoodleCourseAccess( final MoodleCourseAccess moodleCourseAccess = new MoodleCourseAccess(
new JSONMapper(), new JSONMapper(),
null,
moodleRestTemplateFactory, moodleRestTemplateFactory,
null, null,
mock(AsyncService.class), mock(AsyncService.class),
@ -120,7 +119,6 @@ public class MoodleCourseAccessTest {
final MoodleCourseAccess moodleCourseAccess = new MoodleCourseAccess( final MoodleCourseAccess moodleCourseAccess = new MoodleCourseAccess(
new JSONMapper(), new JSONMapper(),
null,
moodleRestTemplateFactory, moodleRestTemplateFactory,
null, null,
mock(AsyncService.class), mock(AsyncService.class),
@ -143,7 +141,6 @@ public class MoodleCourseAccessTest {
final MoodleCourseAccess moodleCourseAccess = new MoodleCourseAccess( final MoodleCourseAccess moodleCourseAccess = new MoodleCourseAccess(
new JSONMapper(), new JSONMapper(),
null,
moodleRestTemplateFactory, moodleRestTemplateFactory,
null, null,
mock(AsyncService.class), mock(AsyncService.class),
@ -165,7 +162,6 @@ public class MoodleCourseAccessTest {
final MoodleCourseAccess moodleCourseAccess = new MoodleCourseAccess( final MoodleCourseAccess moodleCourseAccess = new MoodleCourseAccess(
new JSONMapper(), new JSONMapper(),
null,
moodleRestTemplateFactory, moodleRestTemplateFactory,
null, null,
mock(AsyncService.class), mock(AsyncService.class),