From dd6150ec2a3b62298f3d9b8009d293a1157ee4e5 Mon Sep 17 00:00:00 2001
From: anhefti
Date: Fri, 14 May 2021 17:08:29 +0200
Subject: [PATCH] refactoring and improvement of LMS binding API
---
.../ch/ethz/seb/sebserver/gbl/api/API.java | 1 +
.../seb/sebserver/gbl/model/exam/Exam.java | 9 +-
.../sebserver/gbl/model/exam/QuizData.java | 1 +
.../gbl/model/institution/LmsSetup.java | 16 ++
.../seb/sebserver/gui/content/ExamForm.java | 2 +
.../servicelayer/dao/LmsSetupDAO.java | 1 +
.../servicelayer/dao/impl/ExamDAOImpl.java | 1 +
.../servicelayer/exam/ExamAdminService.java | 36 ----
.../lms/APITemplateDataSupplier.java | 33 +++
.../servicelayer/lms/LmsAPIService.java | 3 +
.../servicelayer/lms/LmsAPITemplate.java | 145 +++++++++----
.../lms/LmsAPITemplateFactory.java | 17 +-
.../lms/SEBRestrictionService.java | 7 +
...eAccess.java => AbstractCourseAccess.java} | 6 +-
.../lms/impl/LmsAPIServiceImpl.java | 185 +++++++++++------
.../lms/impl/LmsSetupChangeEvent.java | 27 ---
.../lms/impl/SEBRestrictionServiceImpl.java | 66 ++++--
.../lms/impl/edx/OpenEdxCourseAccess.java | 85 ++++++--
.../impl/edx/OpenEdxCourseRestriction.java | 190 +++++++++++-------
.../lms/impl/edx/OpenEdxLmsAPITemplate.java | 16 +-
.../edx/OpenEdxLmsAPITemplateFactory.java | 16 +-
.../impl/edx/OpenEdxRestTemplateFactory.java | 45 +++--
.../MockLmsAPITemplateFactory.java | 15 +-
.../{ => mockup}/MockupLmsAPITemplate.java | 39 ++--
.../lms/impl/moodle/MoodleCourseAccess.java | 55 ++---
.../lms/impl/moodle/MoodleLmsAPITemplate.java | 13 +-
.../moodle/MoodleLmsAPITemplateFactory.java | 22 +-
.../moodle/MoodleRestTemplateFactory.java | 127 ++++++------
.../session/impl/ExamSessionServiceImpl.java | 39 +---
.../api/ExamAdministrationController.java | 71 ++++---
.../weblayer/api/LmsSetupController.java | 8 -
.../gbl/model/ModelObjectJSONGenerator.java | 2 +-
.../integration/UseCasesIntegrationTest.java | 1 +
.../integration/api/admin/ExamAPITest.java | 2 +
.../integration/api/admin/ExamImportTest.java | 28 ++-
.../impl/moodle/MoodleCourseAccessTest.java | 4 -
36 files changed, 790 insertions(+), 544 deletions(-)
create mode 100644 src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/APITemplateDataSupplier.java
rename src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/{CourseAccess.java => AbstractCourseAccess.java} (98%)
delete mode 100644 src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsSetupChangeEvent.java
rename src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/{ => mockup}/MockLmsAPITemplateFactory.java (71%)
rename src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/{ => mockup}/MockupLmsAPITemplate.java (88%)
diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java b/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java
index 110b2ce0..3f1a67a0 100644
--- a/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java
+++ b/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java
@@ -129,6 +129,7 @@ public final class API {
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_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_CHECK_RESTRICTION_PATH_SEGMENT = "/check-seb-restriction";
public static final String EXAM_ADMINISTRATION_CHECK_IMPORTED_PATH_SEGMENT = "/check-imported";
diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java
index d9a73d4c..ffb03ef8 100644
--- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java
+++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java
@@ -47,7 +47,7 @@ public final class Exam implements GrantEntity {
null,
null,
ExamStatus.FINISHED,
-// Boolean.FALSE,
+ Boolean.FALSE,
null,
Boolean.FALSE,
null);
@@ -115,6 +115,9 @@ public final class Exam implements GrantEntity {
@JsonProperty(EXAM.ATTR_STATUS)
public final ExamStatus status;
+ @JsonProperty(EXAM.ATTR_LMS_SEB_RESTRICTION)
+ public final Boolean sebRestriction;
+
@JsonProperty(EXAM.ATTR_BROWSER_KEYS)
public final String browserExamKeys;
@@ -139,6 +142,7 @@ public final class Exam implements GrantEntity {
@JsonProperty(EXAM.ATTR_OWNER) final String owner,
@JsonProperty(EXAM.ATTR_SUPPORTER) final Collection supporter,
@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_ACTIVE) final Boolean active,
@JsonProperty(EXAM.ATTR_LASTUPDATE) final String lastUpdate) {
@@ -155,6 +159,7 @@ public final class Exam implements GrantEntity {
this.type = type;
this.owner = owner;
this.status = (status != null) ? status : getStatusFromDate(startTime, endTime);
+ this.sebRestriction = sebRestriction;
this.browserExamKeys = browserExamKeys;
this.active = (active != null) ? active : Boolean.TRUE;
this.lastUpdate = lastUpdate;
@@ -181,6 +186,7 @@ public final class Exam implements GrantEntity {
EXAM.ATTR_STATUS,
ExamStatus.class,
getStatusFromDate(this.startTime, this.endTime));
+ this.sebRestriction = null;
this.browserExamKeys = mapper.getString(EXAM.ATTR_BROWSER_KEYS);
this.active = mapper.getBoolean(EXAM.ATTR_ACTIVE);
this.supporter = mapper.getStringSet(EXAM.ATTR_SUPPORTER);
@@ -204,6 +210,7 @@ public final class Exam implements GrantEntity {
this.type = null;
this.owner = null;
this.status = (status != null) ? status : getStatusFromDate(this.startTime, this.endTime);
+ this.sebRestriction = null;
this.browserExamKeys = null;
this.active = null;
this.supporter = null;
diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/QuizData.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/QuizData.java
index 474c880c..68ea3ac1 100644
--- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/QuizData.java
+++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/QuizData.java
@@ -29,6 +29,7 @@ import ch.ethz.seb.sebserver.gbl.util.Utils;
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 ATTR_ADDITIONAL_ATTRIBUTES = "ADDITIONAL_ATTRIBUTES";
diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/LmsSetup.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/LmsSetup.java
index 70a775b7..4a93f386 100644
--- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/LmsSetup.java
+++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/LmsSetup.java
@@ -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_TYPE = "lms_type";
+ /** LMS binding and API 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,
+ /** 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
}
+ /** Defines the supported types if LMS bindings.
+ * Also defines the supports feature(s) for each type of LMS binding. */
public enum LmsType {
+ /** Mockup LMS type used to create test setups */
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),
+ /** The Moodle binding features only the course access API so far */
MOODLE(Features.COURSE_API /* , Features.SEB_RESTRICTION */),
+ /** The Ans Delft binding is on the way */
ANS_DELFT(/* Features.COURSE_API , Features.SEB_RESTRICTION */),
+ /** The OpenOLAT binding is on the way */
OPEN_OLAT(/* Features.COURSE_API , Features.SEB_RESTRICTION */);
public final EnumSet features;
diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamForm.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamForm.java
index d7494bd6..ed8a7046 100644
--- a/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamForm.java
+++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamForm.java
@@ -226,6 +226,8 @@ public class ExamForm implements TemplateComposer {
final ExamStatus examStatus = exam.getStatus();
final boolean editable = modifyGrant && (examStatus == ExamStatus.UP_COMING ||
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 isRestricted = readonly && sebRestrictionAvailable && this.restService
.getBuilder(CheckSEBRestriction.class)
diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/LmsSetupDAO.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/LmsSetupDAO.java
index 771587f7..ffe0432e 100644
--- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/LmsSetupDAO.java
+++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/LmsSetupDAO.java
@@ -29,4 +29,5 @@ public interface LmsSetupDAO extends ActivatableEntityDAO, B
* @param lmsSetupId the LMS Setup identifier
* @return Result refer to the proxy data or to an error if happened */
Result getLmsAPIAccessProxyData(String lmsSetupId);
+
}
diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamDAOImpl.java
index 0ed5636f..99ce6240 100644
--- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamDAOImpl.java
+++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamDAOImpl.java
@@ -1010,6 +1010,7 @@ public class ExamDAOImpl implements ExamDAO {
record.getOwner(),
supporter,
(quizData != null) ? status : (statusOverride != null) ? statusOverride : status,
+ BooleanUtils.toBooleanObject(record.getLmsSebRestriction()),
record.getBrowserKeys(),
BooleanUtils.toBooleanObject((quizData != null) ? record.getActive() : null),
record.getLastupdate());
diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamAdminService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamAdminService.java
index f9480b8a..51314f64 100644
--- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamAdminService.java
+++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamAdminService.java
@@ -41,17 +41,6 @@ public interface ExamAdminService {
* @return Result refer to the restriction flag or to an error when happened */
Result 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 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.
*
* @param examId the exam identifier
@@ -99,29 +88,4 @@ public interface ExamAdminService {
.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 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 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 getExamProctoringService(final Long examId) {
-// return getProctoringServiceSettings(examId)
-// .flatMap(this::getExamProctoringService);
-// }
-
}
diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/APITemplateDataSupplier.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/APITemplateDataSupplier.java
new file mode 100644
index 00000000..5dad13a6
--- /dev/null
+++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/APITemplateDataSupplier.java
@@ -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();
+
+}
diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/LmsAPIService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/LmsAPIService.java
index 468e424a..3567428e 100644
--- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/LmsAPIService.java
+++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/LmsAPIService.java
@@ -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. */
public interface LmsAPIService {
+ /** Reset and cleanup the caches if there are some */
+ void cleanup();
+
/** Get the specified LmsSetup model by primary key
*
* @param id The identifier (PK) of the LmsSetup model
diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/LmsAPITemplate.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/LmsAPITemplate.java
index 2b556db4..aec35ad1 100644
--- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/LmsAPITemplate.java
+++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/LmsAPITemplate.java
@@ -17,6 +17,7 @@ import java.util.Set;
import org.apache.commons.lang3.StringUtils;
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.Exam;
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.webservice.servicelayer.dao.FilterMap;
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.
- *
+ *
* 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
- * 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
- * a specific LMS instance of implemented type.
- *
- * The enum LmsSetup.LmsType defines the supported LMS types and for each type the supported API part(s).
- *
- * SEB Server uses the test functions that are defined for each LMS API part to test API access for a certain LMS
- * instance respectively the underling LMSSetup. Concrete implementations can do various tests to check full
- * or partial API Access and can flag missing or wrong LMSSetup attributes with the resulting LmsSetupTestResult.
+ *
+ * - The course API to search and request course data from LMS as well as resolve some
+ * LMS account details for a given examineeId.
+ * - The SEB restriction API to apply SEB restriction data to the LMS to restrict a
+ * certain course for SEB.
+ *
+ *
*
+ * Course API
+ * 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.
+ *
+ * Since the course API requests course data from potentially thousands of existing and
+ * active courses, the course API can implement some caches if needed.
+ * 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.
+ * Therefore usual get quiz data functions like {@link #getQuizzes(FilterMap filterMap) },
+ * {@link #getQuizzes(Set 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.
+ * 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.
+ * On the other hand, dedicated cache access functions like {@link #getQuizzesFromCache(Set 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.
+ *
+ * SEB restriction API
+ * 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.
+ *
+ *
+ * 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.
+ * 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.
+ *
+ * The enum {@link LmsSetup.LmsType } defines the supported LMS types and for each type the supported API part(s).
+ *
+ * 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 }.
* SEB Server than uses an instance of this template to communicate with the an LMS. */
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();
- /** 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,
* 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();
- /** Performs a test for the underling 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.
+ /** Get an unsorted List of filtered {@link QuizData } from the LMS course/quiz API
*
- * @return LmsSetupTestResult instance with the test result report */
- LmsSetupTestResult testCourseRestrictionAPI();
-
- /** Get an unsorted List of filtered QuizData from the LMS course/quiz API
+ * @param filterMap the {@link FilterMap } to get a filtered result. Possible filter attributes are:
*
- * @param filterMap the FilterMap to get a filtered result. For possible filter attributes
- * see documentation on QuizData
- * @return Result of an unsorted List of filtered QuizData from the LMS course/quiz API
+ *
+ * {@link QuizData.FILTER_ATTR_QUIZ_NAME } The quiz name filter text (exclude all names that do not contain the given text)
+ * {@link QuizData.FILTER_ATTR_START_TIME } The quiz start time (exclude all quizzes that starts before)
+ *
+ *
+ * @return Result of an unsorted List of filtered {@link QuizData } from the LMS course/quiz API
* or refer to an error when happened */
Result> getQuizzes(FilterMap filterMap);
- /** Get all QuizData for the set of QuizData identifiers from LMS API in a collection
- * of Result. If particular Quiz cannot be loaded because of errors or deletion,
+ /** 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,
* the Result will have an error reference.
*
- * @param ids the Set of Quiz identifiers to get the QuizData for
- * @return Collection of all QuizData from the given id set */
+ * @param ids the Set of Quiz identifiers to get the {@link QuizData } for
+ * @return Collection of all {@link QuizData } from the given id set */
Collection> getQuizzes(Set ids);
/** Get the quiz data with specified identifier.
*
- * Default implementation: Uses getQuizzes(Set ids) and returns the first matching or an error.
+ * Default implementation: Uses {@link #getQuizzes(Set ids) } and returns the first matching or an error.
*
* @param id the quiz data identifier
* @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)));
}
- /** Get all QuizData for the set of QuizData-identifiers (ids) from the LMS defined within the
- * underling LMSSetup, in a collection of Results.
+ /** 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.
*
* 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
* 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 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
- * @return Collection of all QuizData from the given id set */
+ * @param ids the Set of Quiz identifiers to get the {@link QuizData } for
+ * @return Collection of all {@link QuizData } from the given id set */
Collection> getQuizzesFromCache(Set ids);
/** 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
*
* @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 getExamineeAccountDetails(String examineeUserId);
/** 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. */
Result 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).
*
* @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 */
Result getSEBClientRestriction(Exam exam);
@@ -154,7 +218,8 @@ public interface LmsAPITemplate {
*
* @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
- * @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 applySEBClientRestriction(
String externalExamId,
SEBRestriction sebRestrictionData);
diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/LmsAPITemplateFactory.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/LmsAPITemplateFactory.java
index 0af397a8..518b1b74 100644
--- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/LmsAPITemplateFactory.java
+++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/LmsAPITemplateFactory.java
@@ -8,9 +8,6 @@
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.util.Result;
@@ -23,16 +20,10 @@ public interface LmsAPITemplateFactory {
* @return the LMS type if a specific implementation */
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 credentials the access data for accessing the LMS API. Either client credentials or access token from LMS
- * 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 create(
- final LmsSetup lmsSetup,
- final ClientCredentials credentials,
- final ProxyData proxyData);
+ * @param apiTemplateDataSupplier supplies all needed actual LMS setup data */
+ Result create(final APITemplateDataSupplier apiTemplateDataSupplier);
}
diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/SEBRestrictionService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/SEBRestrictionService.java
index 3cea6a00..57558816 100644
--- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/SEBRestrictionService.java
+++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/SEBRestrictionService.java
@@ -8,6 +8,8 @@
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.SEBRestriction;
import ch.ethz.seb.sebserver.gbl.util.Result;
@@ -19,6 +21,9 @@ public interface SEBRestrictionService {
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.
*
* @param exam the Exam
@@ -50,4 +55,6 @@ public interface SEBRestrictionService {
* @return Result refer to the Exam instance or to an error if happened */
Result releaseSEBClientRestriction(Exam exam);
+ boolean checkConsistency(@NotNull Long lmsSetupId, Exam exam);
+
}
diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/CourseAccess.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/AbstractCourseAccess.java
similarity index 98%
rename from src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/CourseAccess.java
rename to src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/AbstractCourseAccess.java
index a4467257..b43fb874 100644
--- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/CourseAccess.java
+++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/AbstractCourseAccess.java
@@ -35,9 +35,9 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
* API requests in a protected environment.
*
* 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
* concrete implementation has such. */
@@ -54,7 +54,7 @@ public abstract class CourseAccess {
/** CircuitBreaker for protected examinee account details requests */
protected final CircuitBreaker accountDetailRequest;
- protected CourseAccess(
+ protected AbstractCourseAccess(
final AsyncService asyncService,
final Environment environment) {
diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsAPIServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsAPIServiceImpl.java
index df4728c4..407bcd40 100644
--- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsAPIServiceImpl.java
+++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsAPIServiceImpl.java
@@ -22,10 +22,10 @@ import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy;
-import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
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.ClientCredentials;
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.webservice.servicelayer.dao.FilterMap;
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.LmsAPITemplate;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplateFactory;
@@ -71,21 +73,9 @@ public class LmsAPIServiceImpl implements LmsAPIService {
this.templateFactories = new EnumMap<>(factories);
}
- /** Listen to LmsSetupChangeEvent to release an affected LmsAPITemplate from cache
- *
- * @param event the event holding the changed LmsSetup */
- @EventListener
- public void notifyLmsSetupChange(final LmsSetupChangeEvent event) {
- final LmsSetup lmsSetup = event.getLmsSetup();
- if (lmsSetup == null) {
- return;
- }
-
- if (log.isDebugEnabled()) {
- log.debug("LmsSetup changed. Update cache by removing eventually used references");
- }
-
- this.cache.remove(new CacheKey(lmsSetup.getModelId(), 0));
+ @Override
+ public void cleanup() {
+ this.cache.clear();
}
@Override
@@ -107,10 +97,19 @@ public class LmsAPIServiceImpl implements LmsAPIService {
@Override
public Result getLmsAPITemplate(final String lmsSetupId) {
- return Result.tryCatch(() -> this.lmsSetupDAO
- .byModelId(lmsSetupId)
- .getOrThrow())
- .flatMap(this::getLmsAPITemplate);
+ return Result.tryCatch(() -> {
+ LmsAPITemplate lmsAPITemplate = getFromCache(lmsSetupId);
+ if (lmsAPITemplate == null) {
+ 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
@@ -129,23 +128,22 @@ public class LmsAPIServiceImpl implements LmsAPIService {
@Override
public LmsSetupTestResult testAdHoc(final LmsSetup lmsSetup) {
- final ClientCredentials lmsCredentials = this.clientCredentialService.encryptClientCredentials(
- lmsSetup.lmsAuthName,
- lmsSetup.lmsAuthSecret,
- lmsSetup.lmsRestApiToken)
- .getOrThrow();
+ final AdHocAPITemplateDataSupplier apiTemplateDataSupplier = new AdHocAPITemplateDataSupplier(
+ lmsSetup,
+ this.clientCredentialService);
- final ProxyData proxyData = (StringUtils.isNoneBlank(lmsSetup.proxyHost))
- ? new ProxyData(
- lmsSetup.proxyHost,
- lmsSetup.proxyPort,
- this.clientCredentialService.encryptClientCredentials(
- lmsSetup.proxyAuthUsername,
- lmsSetup.proxyAuthSecret)
- .getOrThrow())
- : null;
+ final LmsAPITemplate lmsSetupTemplate = createLmsSetupTemplate(apiTemplateDataSupplier);
- 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.
@@ -183,6 +181,7 @@ public class LmsAPIServiceImpl implements LmsAPIService {
return this.lmsSetupDAO.all(institutionId, true)
.getOrThrow()
.parallelStream()
+ .map(LmsSetup::getModelId)
.map(this::getLmsAPITemplate)
.flatMap(Result::onErrorLogAndSkip)
.map(template -> template.getQuizzes(filterMap))
@@ -193,18 +192,7 @@ public class LmsAPIServiceImpl implements LmsAPIService {
});
}
- private Result getLmsAPITemplate(final LmsSetup lmsSetup) {
- 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) {
+ private LmsAPITemplate getFromCache(final String lmsSetupId) {
// first cleanup the cache by removing old instances
final long currentTimeMillis = System.currentTimeMillis();
new ArrayList<>(this.cache.keySet())
@@ -212,40 +200,103 @@ public class LmsAPIServiceImpl implements LmsAPIService {
.filter(key -> key.creationTimestamp - currentTimeMillis > Constants.DAY_IN_MILLIS)
.forEach(this.cache::remove);
// 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()) {
- log.debug("Create new LmsAPITemplate for id: {}", lmsSetup.getModelId());
+ log.debug("Create new LmsAPITemplate for id: {}", lmsSetupId);
}
- final ClientCredentials credentials = this.lmsSetupDAO
- .getLmsAPIAccessCredentials(lmsSetup.getModelId())
- .getOrThrow();
-
- final ProxyData proxyData = this.lmsSetupDAO
- .getLmsAPIAccessProxyData(lmsSetup.getModelId())
- .getOr(null);
-
- return createLmsSetupTemplate(lmsSetup, credentials, proxyData);
+ return createLmsSetupTemplate(new PersistentAPITemplateDataSupplier(
+ lmsSetupId,
+ this.lmsSetupDAO));
}
- private LmsAPITemplate createLmsSetupTemplate(
- final LmsSetup lmsSetup,
- final ClientCredentials credentials,
- final ProxyData proxyData) {
+ private LmsAPITemplate createLmsSetupTemplate(final APITemplateDataSupplier apiTemplateDataSupplier) {
- if (!this.templateFactories.containsKey(lmsSetup.lmsType)) {
- throw new UnsupportedOperationException("No support for LMS Type: " + lmsSetup.lmsType);
+ final LmsType lmsType = apiTemplateDataSupplier.getLmsSetup().lmsType;
+
+ if (!this.templateFactories.containsKey(lmsType)) {
+ throw new UnsupportedOperationException("No support for LMS Type: " + lmsType);
}
- final LmsAPITemplateFactory lmsAPITemplateFactory = this.templateFactories.get(lmsSetup.lmsType);
- return lmsAPITemplateFactory.create(lmsSetup, credentials, proxyData)
+ final LmsAPITemplateFactory lmsAPITemplateFactory = this.templateFactories
+ .get(lmsType);
+
+ return lmsAPITemplateFactory
+ .create(apiTemplateDataSupplier)
.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 {
final String lmsSetupId;
final long creationTimestamp;
diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsSetupChangeEvent.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsSetupChangeEvent.java
deleted file mode 100644
index e3b26315..00000000
--- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsSetupChangeEvent.java
+++ /dev/null
@@ -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;
- }
-
-}
diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/SEBRestrictionServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/SEBRestrictionServiceImpl.java
index 3b3f6d01..0c2c85a8 100644
--- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/SEBRestrictionServiceImpl.java
+++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/SEBRestrictionServiceImpl.java
@@ -18,6 +18,8 @@ import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
+import javax.validation.constraints.NotNull;
+
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
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.model.exam.Exam;
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.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result;
@@ -62,15 +65,38 @@ public class SEBRestrictionServiceImpl implements SEBRestrictionService {
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
@Transactional
public Result getSEBRestrictionFromExam(final Exam exam) {
return Result.tryCatch(() -> {
// load the config keys from restriction and merge with new generated config keys
+ final long currentTimeMillis = System.currentTimeMillis();
final Set configKeys = new HashSet<>();
final Collection generatedKeys = this.examConfigService
.generateConfigKeys(exam.institutionId, exam.id)
.getOrThrow();
+ System.out.println("******* " + (System.currentTimeMillis() - currentTimeMillis));
configKeys.addAll(generatedKeys);
if (generatedKeys != null && !generatedKeys.isEmpty()) {
@@ -134,6 +160,7 @@ public class SEBRestrictionServiceImpl implements SEBRestrictionService {
null, null, null, null, null, null, null, null, null, null,
exam.supporter,
exam.status,
+ null,
(browserExamKeys != null && !browserExamKeys.isEmpty())
? StringUtils.join(browserExamKeys, Constants.LIST_SEPARATOR_CHAR)
: StringUtils.EMPTY,
@@ -167,28 +194,31 @@ public class SEBRestrictionServiceImpl implements SEBRestrictionService {
@Override
public Result applySEBClientRestriction(final Exam exam) {
- if (!this.lmsAPIService
- .getLmsSetup(exam.lmsSetupId)
- .getOrThrow().lmsType.features.contains(Features.SEB_RESTRICTION)) {
+ return Result.tryCatch(() -> {
+ if (!this.lmsAPIService
+ .getLmsSetup(exam.lmsSetupId)
+ .getOrThrow().lmsType.features.contains(Features.SEB_RESTRICTION)) {
- return Result.of(exam);
- }
+ return exam;
+ }
- return this.getSEBRestrictionFromExam(exam)
- .map(sebRestrictionData -> {
+ return this.getSEBRestrictionFromExam(exam)
+ .map(sebRestrictionData -> {
- if (log.isDebugEnabled()) {
- log.debug("Applying SEB Client restriction on LMS with: {}", sebRestrictionData);
- }
+ if (log.isDebugEnabled()) {
+ log.debug("Applying SEB Client restriction on LMS with: {}", sebRestrictionData);
+ }
- return this.lmsAPIService
- .getLmsAPITemplate(exam.lmsSetupId)
- .flatMap(lmsTemplate -> lmsTemplate.applySEBClientRestriction(
- exam.externalId,
- sebRestrictionData))
- .map(data -> exam)
- .getOrThrow();
- });
+ return this.lmsAPIService
+ .getLmsAPITemplate(exam.lmsSetupId)
+ .flatMap(lmsTemplate -> lmsTemplate.applySEBClientRestriction(
+ exam.externalId,
+ sebRestrictionData))
+ .map(data -> exam)
+ .getOrThrow();
+ })
+ .getOrThrow();
+ });
}
@Override
diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxCourseAccess.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxCourseAccess.java
index 69af7ac4..54d36cd3 100644
--- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxCourseAccess.java
+++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxCourseAccess.java
@@ -10,6 +10,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.edx;
import java.net.URL;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
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.webservice.WebserviceInfo;
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.
*
* 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);
@@ -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 final JSONMapper jsonMapper;
- private final LmsSetup lmsSetup;
private final OpenEdxRestTemplateFactory openEdxRestTemplateFactory;
private final WebserviceInfo webserviceInfo;
private final MemoizingCircuitBreaker> allQuizzesRequest;
@@ -80,7 +81,6 @@ final class OpenEdxCourseAccess extends CourseAccess {
public OpenEdxCourseAccess(
final JSONMapper jsonMapper,
- final LmsSetup lmsSetup,
final OpenEdxRestTemplateFactory openEdxRestTemplateFactory,
final WebserviceInfo webserviceInfo,
final AsyncService asyncService,
@@ -88,7 +88,6 @@ final class OpenEdxCourseAccess extends CourseAccess {
super(asyncService, environment);
this.jsonMapper = jsonMapper;
- this.lmsSetup = lmsSetup;
this.openEdxRestTemplateFactory = openEdxRestTemplateFactory;
this.webserviceInfo = webserviceInfo;
@@ -130,8 +129,14 @@ final class OpenEdxCourseAccess extends CourseAccess {
};
}
+ APITemplateDataSupplier getApiTemplateDataSupplier() {
+ return this.openEdxRestTemplateFactory.apiTemplateDataSupplier;
+ }
+
LmsSetupTestResult initAPIAccess() {
+ final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup();
+
final LmsSetupTestResult attributesCheck = this.openEdxRestTemplateFactory.test();
if (!attributesCheck.isOk()) {
return attributesCheck;
@@ -148,13 +153,14 @@ final class OpenEdxCourseAccess extends CourseAccess {
final OAuth2RestTemplate restTemplate = restTemplateRequest.get();
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) {
restTemplate.setAuthenticator(new EdxOAuth2RequestAuthenticator());
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) {
log.error("Failed to access Open edX course API: ", ee);
return LmsSetupTestResult.ofQuizAccessAPIError(ee.getMessage());
@@ -167,14 +173,18 @@ final class OpenEdxCourseAccess extends CourseAccess {
@Override
public Result getExamineeAccountDetails(final String examineeSessionId) {
return Result.tryCatch(() -> {
+ final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup();
+ final HttpHeaders httpHeaders = new HttpHeaders();
final OAuth2RestTemplate template = getRestTemplate()
.getOrThrow();
- final String externalStartURI = this.webserviceInfo.getLmsExternalAddressAlias(this.lmsSetup.lmsApiUrl);
+ final String externalStartURI = this.webserviceInfo
+ .getLmsExternalAddressAlias(lmsSetup.lmsApiUrl);
+
final String uri = (externalStartURI != null)
? externalStartURI + OPEN_EDX_DEFAULT_USER_PROFILE_ENDPOINT + examineeSessionId
- : this.lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_USER_PROFILE_ENDPOINT + examineeSessionId;
- final HttpHeaders httpHeaders = new HttpHeaders();
+ : lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_USER_PROFILE_ENDPOINT + examineeSessionId;
+
final String responseJSON = template.exchange(
uri,
HttpMethod.GET,
@@ -211,9 +221,24 @@ final class OpenEdxCourseAccess extends CourseAccess {
@Override
protected Supplier> quizzesSupplier(final Set ids) {
- return () -> getRestTemplate()
- .map(template -> this.collectQuizzes(template, ids))
- .getOrThrow();
+
+ if (ids.size() == 1) {
+ 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> quizzesSupplier() {
@@ -225,8 +250,10 @@ final class OpenEdxCourseAccess extends CourseAccess {
@Override
protected Supplier getCourseChaptersSupplier(final String courseId) {
return () -> {
+ final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup();
+
final String uri =
- this.lmsSetup.lmsApiUrl +
+ lmsSetup.lmsApiUrl +
OPEN_EDX_DEFAULT_BLOCKS_ENDPOINT +
Utils.encodeFormURL_UTF_8(courseId);
return new Chapters(getCourseBlocks(uri)
@@ -239,16 +266,18 @@ final class OpenEdxCourseAccess extends CourseAccess {
}
private ArrayList collectQuizzes(final OAuth2RestTemplate restTemplate, final Set ids) {
- final String externalStartURI = getExternalLMSServerAddress(this.lmsSetup);
+ final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup();
+ final String externalStartURI = getExternalLMSServerAddress(lmsSetup);
+
return collectCourses(
- this.lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_ENDPOINT,
+ lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_ENDPOINT,
restTemplate,
ids)
.stream()
.reduce(
new ArrayList<>(),
(list, courseData) -> {
- list.add(quizDataOf(this.lmsSetup, courseData, externalStartURI));
+ list.add(quizDataOf(lmsSetup, courseData, externalStartURI));
return list;
},
(list1, list2) -> {
@@ -258,15 +287,16 @@ final class OpenEdxCourseAccess extends CourseAccess {
}
private ArrayList collectAllQuizzes(final OAuth2RestTemplate restTemplate) {
- final String externalStartURI = getExternalLMSServerAddress(this.lmsSetup);
+ final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup();
+ final String externalStartURI = getExternalLMSServerAddress(lmsSetup);
return collectAllCourses(
- this.lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_ENDPOINT,
+ lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_ENDPOINT,
restTemplate)
.stream()
.reduce(
new ArrayList<>(),
(list, courseData) -> {
- list.add(quizDataOf(this.lmsSetup, courseData, externalStartURI));
+ list.add(quizDataOf(lmsSetup, courseData, externalStartURI));
return list;
},
(list1, list2) -> {
@@ -322,6 +352,21 @@ final class OpenEdxCourseAccess extends CourseAccess {
return collector;
}
+ private CourseData getOneCourses(
+ final String pageURI,
+ final OAuth2RestTemplate restTemplate,
+ final String id) {
+
+ final HttpHeaders httpHeaders = new HttpHeaders();
+ final ResponseEntity exchange = restTemplate.exchange(
+ pageURI + "/" + id,
+ HttpMethod.GET,
+ new HttpEntity<>(httpHeaders),
+ CourseData.class);
+
+ return exchange.getBody();
+ }
+
private List collectAllCourses(final String pageURI, final OAuth2RestTemplate restTemplate) {
final List collector = new ArrayList<>();
EdXPage page = getEdxPage(pageURI, restTemplate).getBody();
diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxCourseRestriction.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxCourseRestriction.java
index 7fc91d32..7176937d 100644
--- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxCourseRestriction.java
+++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxCourseRestriction.java
@@ -8,8 +8,6 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.edx;
-import java.util.function.BooleanSupplier;
-
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpEntity;
@@ -23,8 +21,6 @@ import org.springframework.web.client.HttpClientErrorException;
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.model.exam.OpenEdxSEBRestriction;
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 =
"/seb-openedx/api/v1/course/%s/configuration/";
- private final LmsSetup lmsSetup;
private final JSONMapper jsonMapper;
private final OpenEdxRestTemplateFactory openEdxRestTemplateFactory;
private OAuth2RestTemplate restTemplate;
protected OpenEdxCourseRestriction(
- final LmsSetup lmsSetup,
final JSONMapper jsonMapper,
final OpenEdxRestTemplateFactory openEdxRestTemplateFactory,
final int restrictionAPIPushCount) {
- this.lmsSetup = lmsSetup;
this.jsonMapper = jsonMapper;
this.openEdxRestTemplateFactory = openEdxRestTemplateFactory;
}
@@ -75,15 +68,16 @@ public class OpenEdxCourseRestriction {
}
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 {
+ 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(
url,
@@ -111,8 +105,10 @@ public class OpenEdxCourseRestriction {
log.debug("GET SEB Client restriction on course: {}", courseId);
}
+ final LmsSetup lmsSetup = this.openEdxRestTemplateFactory.apiTemplateDataSupplier.getLmsSetup();
+
return Result.tryCatch(() -> {
- final String url = this.lmsSetup.lmsApiUrl + getSEBRestrictionUrl(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");
@@ -146,9 +142,28 @@ public class OpenEdxCourseRestriction {
log.debug("PUT SEB Client restriction on course: {} : {}", courseId, restriction);
}
- return handleSEBRestriction(pushSEBRestrictionFunction(
- restriction,
- courseId));
+ return Result.tryCatch(() -> {
+ final LmsSetup lmsSetup = this.openEdxRestTemplateFactory.apiTemplateDataSupplier.getLmsSetup();
+ 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 deleteSEBRestriction(final String courseId) {
@@ -157,74 +172,99 @@ public class OpenEdxCourseRestriction {
log.debug("DELETE SEB Client restriction on course: {}", courseId);
}
- return handleSEBRestriction(deleteSEBRestrictionFunction(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 () -> {
+ return Result.tryCatch(() -> {
+ final LmsSetup lmsSetup = this.openEdxRestTemplateFactory.apiTemplateDataSupplier.getLmsSetup();
+ final String url = lmsSetup.lmsApiUrl + getSEBRestrictionUrl(courseId);
final HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate");
- final ResponseEntity