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 7603eabc..c071df70 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
@@ -56,7 +56,7 @@ public final class LmsSetup implements GrantEntity, Activatable {
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 */
+ /** The Open edX LMS binding features both APIs, course access as well as 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 */),
diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/LmsSetupTestResult.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/LmsSetupTestResult.java
index 6cdc41e5..fbef5775 100644
--- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/LmsSetupTestResult.java
+++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/LmsSetupTestResult.java
@@ -28,6 +28,7 @@ public final class LmsSetupTestResult {
public static final String ATTR_MISSING_ATTRIBUTE = "missingLMSSetupAttribute";
public enum ErrorType {
+ API_NOT_SUPPORTED,
MISSING_ATTRIBUTE,
TOKEN_REQUEST,
QUIZ_ACCESS_API_REQUEST,
@@ -109,7 +110,12 @@ public final class LmsSetupTestResult {
return new LmsSetupTestResult(lmsType);
}
- public static LmsSetupTestResult ofMissingAttributes(final LmsSetup.LmsType lmsType,
+ public static LmsSetupTestResult ofAPINotSupported(final LmsSetup.LmsType lmsType) {
+ return new LmsSetupTestResult(lmsType, new Error(ErrorType.TOKEN_REQUEST, "Not Supported"));
+ }
+
+ public static LmsSetupTestResult ofMissingAttributes(
+ final LmsSetup.LmsType lmsType,
final Collection attrs) {
return new LmsSetupTestResult(lmsType, new Error(ErrorType.MISSING_ATTRIBUTE, "missing attribute(s)"), attrs);
}
diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/CourseAccessAPI.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/CourseAccessAPI.java
new file mode 100644
index 00000000..455ffdd7
--- /dev/null
+++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/CourseAccessAPI.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (c) 2022 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 java.util.Collection;
+import java.util.List;
+import java.util.Set;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import ch.ethz.seb.sebserver.gbl.model.exam.Chapters;
+import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
+import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
+import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
+import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails;
+import ch.ethz.seb.sebserver.gbl.util.Result;
+import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
+
+public interface CourseAccessAPI {
+
+ Logger log = LoggerFactory.getLogger(CourseAccessAPI.class);
+
+ /** Fetch status that indicates an asynchronous quiz data fetch status if the
+ * concrete implementation has such. */
+ public enum FetchStatus {
+ ALL_FETCHED,
+ ASYNC_FETCH_RUNNING,
+ FETCH_ERROR
+ }
+
+ /** 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 {@link LmsSetupTestResult } instance with the test result report */
+ LmsSetupTestResult testCourseAccessAPI();
+
+ /** Get an unsorted List of filtered {@link QuizData } from the LMS course/quiz API
+ *
+ * @param filterMap the {@link FilterMap } to get a filtered result. Possible filter attributes are:
+ *
+ *
+ * {@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 {@link QuizData } for the set of {@link QuizData } identifiers from LMS API in a collection
+ * of Result. If particular quizzes cannot be loaded because of errors or deletion,
+ * the the referencing QuizData will not be in the resulting list and an error is logged.
+ *
+ * @param ids the Set of Quiz identifiers to get the {@link QuizData } for
+ * @return Collection of all {@link QuizData } from the given id set */
+ Result> getQuizzes(Set ids);
+
+ /** Get the quiz data with specified identifier.
+ *
+ * @param id the quiz data identifier
+ * @return Result refer to the quiz data or to an error when happened */
+ Result getQuiz(final String id);
+
+ /** Clears the underling caches if there are some for a particular implementation. */
+ void clearCourseCache();
+
+ /** 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 {@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,
+ * to a readable LMS examinee account name by requesting this on the LMS API with the given examineeUserId.
+ *
+ * If the underling concrete template implementation does not support this user name conversion,
+ * the given examineeSessionId shall be returned.
+ *
+ * @param examineeUserId the examinee user identifier derived from SEB Client
+ * @return a user account display name if supported or the given examineeSessionId if not. */
+ String getExamineeName(final String examineeUserId);
+
+ /** Used to get a list of chapters (display name and chapter-identifier) that can be used to
+ * apply chapter-based SEB restriction for a specified course.
+ *
+ * The availability of this depends on the type of LMS and on installed plugins that supports this feature.
+ * If this is not supported by the underling LMS a UnsupportedOperationException will be presented
+ * within the Result.
+ *
+ * @param courseId The course identifier
+ * @return Result referencing to the Chapters model for the given course or to an error when happened. */
+ Result getCourseChapters(String courseId);
+
+ default FetchStatus getFetchStatus() {
+ return FetchStatus.ALL_FETCHED;
+ }
+
+}
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 8b990a67..f6b7d4ee 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
@@ -8,20 +8,9 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.lms;
-import java.util.Collection;
-import java.util.List;
-import java.util.Set;
-
import ch.ethz.seb.sebserver.gbl.async.CircuitBreaker;
-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;
-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.LmsSetupTestResult;
-import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails;
-import ch.ethz.seb.sebserver.gbl.util.Result;
-import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.AbstractCachedCourseAccess;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.AbstractCourseAccess;
@@ -75,7 +64,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.AbstractCourseAcce
* 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 {
+public interface LmsAPITemplate extends CourseAccessAPI, SEBRestrictionAPI {
/** Get the LMS type of the concrete template implementation
*
@@ -87,110 +76,8 @@ public interface LmsAPITemplate {
* @return the underling {@link LmsSetup } configuration for this LmsAPITemplate */
LmsSetup lmsSetup();
- // *******************************************************************
- // **** 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 {@link LmsSetupTestResult } instance with the test result report */
- LmsSetupTestResult testCourseAccessAPI();
-
- /** Get an unsorted List of filtered {@link QuizData } from the LMS course/quiz API
- *
- * @param filterMap the {@link FilterMap } to get a filtered result. Possible filter attributes are:
- *
- *
- * {@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 {@link QuizData } for the set of {@link QuizData } identifiers from LMS API in a collection
- * of Result. If particular quizzes cannot be loaded because of errors or deletion,
- * the the referencing QuizData will not be in the resulting list and an error is logged.
- *
- * @param ids the Set of Quiz identifiers to get the {@link QuizData } for
- * @return Collection of all {@link QuizData } from the given id set */
- Result> getQuizzes(Set ids);
-
- /** Get the quiz data with specified identifier.
- *
- * @param id the quiz data identifier
- * @return Result refer to the quiz data or to an error when happened */
- Result getQuiz(final String id);
-
- /** Clears the underling caches if there are some for a particular implementation. */
- void clearCache();
-
- /** 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 {@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,
- * to a readable LMS examinee account name by requesting this on the LMS API with the given examineeUserId.
- *
- * If the underling concrete template implementation does not support this user name conversion,
- * the given examineeSessionId shall be returned.
- *
- * @param examineeUserId the examinee user identifier derived from SEB Client
- * @return a user account display name if supported or the given examineeSessionId if not. */
- String getExamineeName(String examineeUserId);
-
- /** Used to get a list of chapters (display name and chapter-identifier) that can be used to
- * apply chapter-based SEB restriction for a specified course.
- *
- * The availability of this depends on the type of LMS and on installed plugins that supports this feature.
- * If this is not supported by the underling LMS a UnsupportedOperationException will be presented
- * within the Result.
- *
- * @param courseId The course identifier
- * @return Result referencing to the Chapters model for the given course or to an error when happened. */
- Result getCourseChapters(String courseId);
-
- // ****************************************************************************
- // **** 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 {@link SEBRestrictionData } instance or to an ResourceNotFoundException if the
- * restriction is
- * missing or to another exception on unexpected error case */
- Result getSEBClientRestriction(Exam exam);
-
- /** Applies SEB Client restrictions to the LMS with the given attributes.
- *
- * @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 {@link SEBRestrictionData } if restriction was successful or to an error if
- * not */
- Result applySEBClientRestriction(
- String externalExamId,
- SEBRestriction sebRestrictionData);
-
- /** Releases an already applied SEB Client restriction within the LMS for a given Exam.
- * This completely removes the SEB Client restriction on LMS side.
- *
- * @param exam the Exam to release the restriction for.
- * @return Result refer to the given Exam if successful or to an error if not */
- Result releaseSEBClientRestriction(Exam exam);
+ default void dispose() {
+ clearCourseCache();
+ }
}
diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/SEBRestrictionAPI.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/SEBRestrictionAPI.java
new file mode 100644
index 00000000..681c248c
--- /dev/null
+++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/SEBRestrictionAPI.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2022 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.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.LmsSetupTestResult;
+import ch.ethz.seb.sebserver.gbl.util.Result;
+
+public interface SEBRestrictionAPI {
+
+ /** 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 {@link SEBRestrictionData } instance or to an ResourceNotFoundException if the
+ * restriction is
+ * missing or to another exception on unexpected error case */
+ Result getSEBClientRestriction(Exam exam);
+
+ /** Applies SEB Client restrictions to the LMS with the given attributes.
+ *
+ * @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 {@link SEBRestrictionData } if restriction was successful or to an error if
+ * not */
+ Result applySEBClientRestriction(
+ String externalExamId,
+ SEBRestriction sebRestrictionData);
+
+ /** Releases an already applied SEB Client restriction within the LMS for a given Exam.
+ * This completely removes the SEB Client restriction on LMS side.
+ *
+ * @param exam the Exam to release the restriction for.
+ * @return Result refer to the given Exam if successful or to an error if not */
+ Result releaseSEBClientRestriction(Exam exam);
+
+}
diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/AbstractCachedCourseAccess.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/AbstractCachedCourseAccess.java
index 7e2a186a..1e78e578 100644
--- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/AbstractCachedCourseAccess.java
+++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/AbstractCachedCourseAccess.java
@@ -16,10 +16,8 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
-import org.springframework.core.env.Environment;
import ch.ethz.seb.sebserver.gbl.Constants;
-import ch.ethz.seb.sebserver.gbl.async.AsyncService;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
/** This implements an overall short time cache for QuizData objects for all implementing
@@ -28,7 +26,7 @@ import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
* The QuizData are stored with a key composed from the id of the key
*
* The EH-Cache can be configured in file ehcache.xml **/
-public abstract class AbstractCachedCourseAccess extends AbstractCourseAccess {
+public abstract class AbstractCachedCourseAccess {
private static final Logger log = LoggerFactory.getLogger(AbstractCachedCourseAccess.class);
@@ -37,17 +35,12 @@ public abstract class AbstractCachedCourseAccess extends AbstractCourseAccess {
private final Cache cache;
- protected AbstractCachedCourseAccess(
- final AsyncService asyncService,
- final Environment environment,
- final CacheManager cacheManager) {
-
- super(asyncService, environment);
+ protected AbstractCachedCourseAccess(final CacheManager cacheManager) {
this.cache = cacheManager.getCache(CACHE_NAME_QUIZ_DATA);
}
/** Used to clear the entire cache */
- public void clearCache() {
+ public void clearCourseCache() {
final Object nativeCache = this.cache.getNativeCache();
if (nativeCache instanceof javax.cache.Cache) {
try {
diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/AbstractCourseAccess.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/AbstractCourseAccess.java
index 5a5f3a3c..b374eb59 100644
--- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/AbstractCourseAccess.java
+++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/AbstractCourseAccess.java
@@ -20,7 +20,6 @@ import org.springframework.core.env.Environment;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.async.AsyncService;
import ch.ethz.seb.sebserver.gbl.async.CircuitBreaker;
-import ch.ethz.seb.sebserver.gbl.async.CircuitBreaker.State;
import ch.ethz.seb.sebserver.gbl.model.exam.Chapters;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails;
@@ -35,14 +34,6 @@ public abstract class AbstractCourseAccess {
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. */
- public enum FetchStatus {
- ALL_FETCHED,
- ASYNC_FETCH_RUNNING,
- FETCH_ERROR
- }
-
/** CircuitBreaker for protected quiz and course data requests */
protected final CircuitBreaker> allQuizzesRequest;
/** CircuitBreaker for protected quiz and course data requests */
@@ -194,10 +185,4 @@ public abstract class AbstractCourseAccess {
/** Provides a supplier for the course chapter data request to use within the circuit breaker */
protected abstract Supplier getCourseChaptersSupplier(final String courseId);
- protected FetchStatus getFetchStatus() {
- if (this.quizzesRequest.getState() != State.CLOSED) {
- return FetchStatus.FETCH_ERROR;
- }
- return FetchStatus.ALL_FETCHED;
- }
}
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 f873767b..cc20ce79 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
@@ -96,12 +96,13 @@ public class LmsAPIServiceImpl implements LmsAPIService {
final LmsAPITemplate removedTemplate = this.cache
.remove(new CacheKey(lmsSetup.getModelId(), 0));
if (removedTemplate != null) {
- removedTemplate.clearCache();
+ removedTemplate.clearCourseCache();
}
}
@Override
public void cleanup() {
+ this.cache.values().forEach(LmsAPITemplate::dispose);
this.cache.clear();
}
diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsAPITemplateAdapter.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsAPITemplateAdapter.java
new file mode 100644
index 00000000..365c9452
--- /dev/null
+++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsAPITemplateAdapter.java
@@ -0,0 +1,407 @@
+/*
+ * Copyright (c) 2022 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 java.util.Collection;
+import java.util.List;
+import java.util.Set;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.core.env.Environment;
+
+import ch.ethz.seb.sebserver.gbl.Constants;
+import ch.ethz.seb.sebserver.gbl.async.AsyncService;
+import ch.ethz.seb.sebserver.gbl.async.CircuitBreaker;
+import ch.ethz.seb.sebserver.gbl.async.CircuitBreaker.State;
+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;
+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.LmsType;
+import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
+import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails;
+import ch.ethz.seb.sebserver.gbl.util.Result;
+import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
+import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier;
+import ch.ethz.seb.sebserver.webservice.servicelayer.lms.CourseAccessAPI;
+import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
+import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionAPI;
+
+public class LmsAPITemplateAdapter implements LmsAPITemplate {
+
+ private static final Logger log = LoggerFactory.getLogger(LmsAPITemplateAdapter.class);
+
+ private final CourseAccessAPI courseAccessAPI;
+ private final SEBRestrictionAPI sebBestrictionAPI;
+ private final APITemplateDataSupplier apiTemplateDataSupplier;
+
+ /** CircuitBreaker for protected quiz and course data requests */
+ private final CircuitBreaker> allQuizzesRequest;
+ /** CircuitBreaker for protected quiz and course data requests */
+ private final CircuitBreaker> quizzesRequest;
+ /** CircuitBreaker for protected quiz and course data requests */
+ private final CircuitBreaker quizRequest;
+ /** CircuitBreaker for protected chapter data requests */
+ private final CircuitBreaker chaptersRequest;
+ /** CircuitBreaker for protected examinee account details requests */
+ private final CircuitBreaker accountDetailRequest;
+
+ private final CircuitBreaker restrictionRequest;
+ private final CircuitBreaker releaseRestrictionRequest;
+
+ public LmsAPITemplateAdapter(
+ final AsyncService asyncService,
+ final Environment environment,
+ final APITemplateDataSupplier apiTemplateDataSupplier,
+ final CourseAccessAPI courseAccessAPI,
+ final SEBRestrictionAPI sebBestrictionAPI) {
+
+ this.courseAccessAPI = courseAccessAPI;
+ this.sebBestrictionAPI = sebBestrictionAPI;
+ this.apiTemplateDataSupplier = apiTemplateDataSupplier;
+
+ this.allQuizzesRequest = asyncService.createCircuitBreaker(
+ environment.getProperty(
+ "sebserver.webservice.circuitbreaker.quizzesRequest.attempts",
+ Integer.class,
+ 3),
+ environment.getProperty(
+ "sebserver.webservice.circuitbreaker.quizzesRequest.blockingTime",
+ Long.class,
+ Constants.MINUTE_IN_MILLIS),
+ environment.getProperty(
+ "sebserver.webservice.circuitbreaker.quizzesRequest.timeToRecover",
+ Long.class,
+ Constants.MINUTE_IN_MILLIS));
+
+ this.quizzesRequest = asyncService.createCircuitBreaker(
+ environment.getProperty(
+ "sebserver.webservice.circuitbreaker.quizzesRequest.attempts",
+ Integer.class,
+ 3),
+ environment.getProperty(
+ "sebserver.webservice.circuitbreaker.quizzesRequest.blockingTime",
+ Long.class,
+ Constants.SECOND_IN_MILLIS * 10),
+ environment.getProperty(
+ "sebserver.webservice.circuitbreaker.quizzesRequest.timeToRecover",
+ Long.class,
+ Constants.MINUTE_IN_MILLIS));
+
+ this.quizRequest = asyncService.createCircuitBreaker(
+ environment.getProperty(
+ "sebserver.webservice.circuitbreaker.quizzesRequest.attempts",
+ Integer.class,
+ 3),
+ environment.getProperty(
+ "sebserver.webservice.circuitbreaker.quizzesRequest.blockingTime",
+ Long.class,
+ Constants.SECOND_IN_MILLIS * 10),
+ environment.getProperty(
+ "sebserver.webservice.circuitbreaker.quizzesRequest.timeToRecover",
+ Long.class,
+ Constants.MINUTE_IN_MILLIS));
+
+ this.chaptersRequest = asyncService.createCircuitBreaker(
+ environment.getProperty(
+ "sebserver.webservice.circuitbreaker.chaptersRequest.attempts",
+ Integer.class,
+ 3),
+ environment.getProperty(
+ "sebserver.webservice.circuitbreaker.chaptersRequest.blockingTime",
+ Long.class,
+ Constants.SECOND_IN_MILLIS * 10),
+ environment.getProperty(
+ "sebserver.webservice.circuitbreaker.chaptersRequest.timeToRecover",
+ Long.class,
+ Constants.SECOND_IN_MILLIS * 30));
+
+ this.accountDetailRequest = asyncService.createCircuitBreaker(
+ environment.getProperty(
+ "sebserver.webservice.circuitbreaker.accountDetailRequest.attempts",
+ Integer.class,
+ 2),
+ environment.getProperty(
+ "sebserver.webservice.circuitbreaker.accountDetailRequest.blockingTime",
+ Long.class,
+ Constants.SECOND_IN_MILLIS * 10),
+ environment.getProperty(
+ "sebserver.webservice.circuitbreaker.accountDetailRequest.timeToRecover",
+ Long.class,
+ Constants.SECOND_IN_MILLIS * 30));
+
+ this.restrictionRequest = asyncService.createCircuitBreaker(
+ environment.getProperty(
+ "sebserver.webservice.circuitbreaker.sebrestriction.attempts",
+ Integer.class,
+ 2),
+ environment.getProperty(
+ "sebserver.webservice.circuitbreaker.sebrestriction.blockingTime",
+ Long.class,
+ Constants.SECOND_IN_MILLIS * 10),
+ environment.getProperty(
+ "sebserver.webservice.circuitbreaker.sebrestriction.timeToRecover",
+ Long.class,
+ Constants.SECOND_IN_MILLIS * 30));
+
+ this.releaseRestrictionRequest = asyncService.createCircuitBreaker(
+ environment.getProperty(
+ "sebserver.webservice.circuitbreaker.sebrestriction.attempts",
+ Integer.class,
+ 2),
+ environment.getProperty(
+ "sebserver.webservice.circuitbreaker.sebrestriction.blockingTime",
+ Long.class,
+ Constants.SECOND_IN_MILLIS * 10),
+ environment.getProperty(
+ "sebserver.webservice.circuitbreaker.sebrestriction.timeToRecover",
+ Long.class,
+ Constants.SECOND_IN_MILLIS * 30));
+ }
+
+ @Override
+ public LmsType getType() {
+ return this.lmsSetup().getLmsType();
+ }
+
+ @Override
+ public LmsSetup lmsSetup() {
+ return this.apiTemplateDataSupplier.getLmsSetup();
+ }
+
+ @Override
+ public LmsSetupTestResult testCourseAccessAPI() {
+ if (this.courseAccessAPI != null) {
+ return this.courseAccessAPI.testCourseAccessAPI();
+ }
+
+ if (log.isDebugEnabled()) {
+ log.debug("Test Course Access API for LMSSetup: {}", lmsSetup());
+ }
+
+ return LmsSetupTestResult.ofAPINotSupported(getType());
+ }
+
+ @Override
+ public FetchStatus getFetchStatus() {
+ if (this.courseAccessAPI == null) {
+ return FetchStatus.FETCH_ERROR;
+ }
+
+ if (this.allQuizzesRequest.getState() != State.CLOSED) {
+ return FetchStatus.FETCH_ERROR;
+ }
+
+ return this.courseAccessAPI.getFetchStatus();
+ }
+
+ @Override
+ public Result> getQuizzes(final FilterMap filterMap) {
+
+ if (this.courseAccessAPI == null) {
+ return Result
+ .ofError(new UnsupportedOperationException("Course API Not Supported For: " + getType().name()));
+ }
+
+ if (log.isDebugEnabled()) {
+ log.debug("Get quizzes for LMSSetup: {}", lmsSetup());
+ }
+
+ return this.allQuizzesRequest.protectedRun(() -> this.courseAccessAPI
+ .getQuizzes(filterMap)
+ .onError(error -> log.error(
+ "Failed to run protectedQuizzesRequest: {}",
+ error.getMessage()))
+ .getOrThrow());
+ }
+
+ @Override
+ public Result> getQuizzes(final Set ids) {
+
+ if (this.courseAccessAPI == null) {
+ return Result
+ .ofError(new UnsupportedOperationException("Course API Not Supported For: " + getType().name()));
+ }
+
+ if (log.isDebugEnabled()) {
+ log.debug("Get quizzes {} for LMSSetup: {}", ids, lmsSetup());
+ }
+
+ return this.quizzesRequest.protectedRun(() -> this.courseAccessAPI
+ .getQuizzes(ids)
+ .onError(error -> log.error(
+ "Failed to run protectedQuizzesRequest: {}",
+ error.getMessage()))
+ .getOrThrow());
+ }
+
+ @Override
+ public Result getQuiz(final String id) {
+
+ if (this.courseAccessAPI == null) {
+ return Result
+ .ofError(new UnsupportedOperationException("Course API Not Supported For: " + getType().name()));
+ }
+
+ if (log.isDebugEnabled()) {
+ log.debug("Get quiz {} for LMSSetup: {}", id, lmsSetup());
+ }
+
+ return this.quizRequest.protectedRun(() -> this.courseAccessAPI
+ .getQuiz(id)
+ .onError(error -> log.error(
+ "Failed to run protectedQuizRequest: {}",
+ error.getMessage()))
+ .getOrThrow());
+ }
+
+ @Override
+ public void clearCourseCache() {
+ if (this.courseAccessAPI != null) {
+
+ if (log.isDebugEnabled()) {
+ log.debug("Clear course cache for LMSSetup: {}", lmsSetup());
+ }
+
+ this.courseAccessAPI.clearCourseCache();
+ }
+ }
+
+ @Override
+ public Result getExamineeAccountDetails(final String examineeUserId) {
+
+ if (this.courseAccessAPI == null) {
+ return Result
+ .ofError(new UnsupportedOperationException("Course API Not Supported For: " + getType().name()));
+ }
+
+ if (log.isDebugEnabled()) {
+ log.debug("Get examinee details {} for LMSSetup: {}", examineeUserId, lmsSetup());
+ }
+
+ return this.accountDetailRequest.protectedRun(() -> this.courseAccessAPI
+ .getExamineeAccountDetails(examineeUserId)
+ .onError(error -> log.error(
+ "Unexpected error while trying to get examinee account details: {}",
+ error.getMessage()))
+ .getOrThrow());
+ }
+
+ @Override
+ public String getExamineeName(final String examineeUserId) {
+
+ if (this.courseAccessAPI == null) {
+ throw new UnsupportedOperationException("Course API Not Supported For: " + getType().name());
+ }
+
+ if (log.isDebugEnabled()) {
+ log.debug("Get examinee name {} for LMSSetup: {}", examineeUserId, lmsSetup());
+ }
+
+ return this.courseAccessAPI.getExamineeName(examineeUserId);
+ }
+
+ @Override
+ public Result getCourseChapters(final String courseId) {
+
+ if (this.courseAccessAPI == null) {
+ return Result
+ .ofError(new UnsupportedOperationException("Course API Not Supported For: " + getType().name()));
+ }
+
+ if (log.isDebugEnabled()) {
+ log.debug("Get course chapters {} for LMSSetup: {}", courseId, lmsSetup());
+ }
+
+ return this.chaptersRequest.protectedRun(() -> this.courseAccessAPI
+ .getCourseChapters(courseId)
+ .onError(error -> log.error(
+ "Failed to run getCourseChapters: {}",
+ error.getMessage()))
+ .getOrThrow());
+ }
+
+ @Override
+ public LmsSetupTestResult testCourseRestrictionAPI() {
+ if (this.sebBestrictionAPI != null) {
+ return this.sebBestrictionAPI.testCourseRestrictionAPI();
+ }
+
+ if (log.isDebugEnabled()) {
+ log.debug("Test course restriction API for LMSSetup: {}", lmsSetup());
+ }
+
+ return LmsSetupTestResult.ofAPINotSupported(getType());
+ }
+
+ @Override
+ public Result getSEBClientRestriction(final Exam exam) {
+
+ if (this.sebBestrictionAPI == null) {
+ return Result.ofError(
+ new UnsupportedOperationException("SEB Restriction API Not Supported For: " + getType().name()));
+ }
+
+ if (log.isDebugEnabled()) {
+ log.debug("Get course restriction: {} for LMSSetup: {}", exam.externalId, lmsSetup());
+ }
+
+ return this.restrictionRequest.protectedRun(() -> this.sebBestrictionAPI
+ .getSEBClientRestriction(exam)
+ .onError(error -> log.error(
+ "Failed to get SEB restrictions: {}",
+ error.getMessage()))
+ .getOrThrow());
+ }
+
+ @Override
+ public Result applySEBClientRestriction(
+ final String externalExamId,
+ final SEBRestriction sebRestrictionData) {
+
+ if (this.sebBestrictionAPI == null) {
+ return Result.ofError(
+ new UnsupportedOperationException("SEB Restriction API Not Supported For: " + getType().name()));
+ }
+
+ if (log.isDebugEnabled()) {
+ log.debug("Apply course restriction: {} for LMSSetup: {}", externalExamId, lmsSetup());
+ }
+
+ return this.restrictionRequest.protectedRun(() -> this.sebBestrictionAPI
+ .applySEBClientRestriction(externalExamId, sebRestrictionData)
+ .onError(error -> log.error(
+ "Failed to apply SEB restrictions: {}",
+ error.getMessage()))
+ .getOrThrow());
+ }
+
+ @Override
+ public Result releaseSEBClientRestriction(final Exam exam) {
+
+ if (this.sebBestrictionAPI == null) {
+ return Result.ofError(
+ new UnsupportedOperationException("SEB Restriction API Not Supported For: " + getType().name()));
+ }
+
+ if (log.isDebugEnabled()) {
+ log.debug("Release course restriction: {} for LMSSetup: {}", exam.externalId, lmsSetup());
+ }
+
+ return this.releaseRestrictionRequest.protectedRun(() -> this.sebBestrictionAPI
+ .releaseSEBClientRestriction(exam)
+ .onError(error -> log.error(
+ "Failed to release SEB restrictions: {}",
+ error.getMessage()))
+ .getOrThrow());
+ }
+
+}
diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsLmsAPITemplate.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsLmsAPITemplate.java
index 078227ac..6f160da9 100644
--- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsLmsAPITemplate.java
+++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsLmsAPITemplate.java
@@ -18,7 +18,6 @@ import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
-import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -28,7 +27,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.CacheManager;
import org.springframework.core.ParameterizedTypeReference;
-import org.springframework.core.env.Environment;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
@@ -39,7 +37,6 @@ import org.springframework.web.client.RestTemplate;
import ch.ethz.seb.sebserver.ClientHttpRequestFactoryService;
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
-import ch.ethz.seb.sebserver.gbl.async.AsyncService;
import ch.ethz.seb.sebserver.gbl.client.ClientCredentialService;
import ch.ethz.seb.sebserver.gbl.client.ClientCredentials;
import ch.ethz.seb.sebserver.gbl.client.ProxyData;
@@ -78,11 +75,9 @@ public class AnsLmsAPITemplate extends AbstractCachedCourseAccess implements Lms
final ClientHttpRequestFactoryService clientHttpRequestFactoryService,
final ClientCredentialService clientCredentialService,
final APITemplateDataSupplier apiTemplateDataSupplier,
- final AsyncService asyncService,
- final Environment environment,
final CacheManager cacheManager) {
- super(asyncService, environment, cacheManager);
+ super(cacheManager);
this.clientHttpRequestFactoryService = clientHttpRequestFactoryService;
this.clientCredentialService = clientCredentialService;
@@ -170,7 +165,7 @@ public class AnsLmsAPITemplate extends AbstractCachedCourseAccess implements Lms
@Override
public Result> getQuizzes(final FilterMap filterMap) {
return this
- .protectedQuizzesRequest(filterMap)
+ .allQuizzesRequest(filterMap)
.map(quizzes -> quizzes.stream()
.filter(LmsAPIService.quizFilterPredicate(filterMap))
.collect(Collectors.toList()));
@@ -191,7 +186,7 @@ public class AnsLmsAPITemplate extends AbstractCachedCourseAccess implements Lms
});
if (!leftIds.isEmpty()) {
- result.addAll(super.protectedQuizzesRequest(leftIds).getOrThrow());
+ result.addAll(quizzesRequest(leftIds).getOrThrow());
}
return result;
@@ -205,7 +200,7 @@ public class AnsLmsAPITemplate extends AbstractCachedCourseAccess implements Lms
return Result.of(fromCache);
}
- return super.protectedQuizRequest(id);
+ return quizRequest(id);
}
private List collectAllQuizzes(final AnsPersonalRestTemplate restTemplate) {
@@ -279,33 +274,28 @@ public class AnsLmsAPITemplate extends AbstractCachedCourseAccess implements Lms
.collect(Collectors.toList());
}
- @Override
- protected Supplier> allQuizzesSupplier(final FilterMap filterMap) {
+ protected Result> allQuizzesRequest(final FilterMap filterMap) {
// We cannot filter by from-date or partial names using the Ans search API.
// Only exact matches are permitted. So we're not implementing filtering
// on the API level and always retrieve all assignments and let SEB server
// do the filtering.
- return () -> {
+ return Result.tryCatch(() -> {
final List res = getRestTemplate()
.map(this::collectAllQuizzes)
.getOrThrow();
super.putToCache(res);
return res;
- };
+ });
}
- @Override
- protected Supplier> quizzesSupplier(final Set ids) {
- return () -> getRestTemplate()
- .map(t -> this.getQuizzesByIds(t, ids))
- .getOrThrow();
+ protected Result> quizzesRequest(final Set ids) {
+ return getRestTemplate()
+ .map(t -> this.getQuizzesByIds(t, ids));
}
- @Override
- protected Supplier quizSupplier(final String id) {
- return () -> getRestTemplate()
- .map(t -> this.getQuizByAssignmentId(t, id))
- .getOrThrow();
+ protected Result quizRequest(final String id) {
+ return getRestTemplate()
+ .map(t -> this.getQuizByAssignmentId(t, id));
}
private ExamineeAccountDetails getExamineeById(final RestTemplate restTemplate, final String id) {
@@ -324,17 +314,21 @@ public class AnsLmsAPITemplate extends AbstractCachedCourseAccess implements Lms
}
@Override
- protected Supplier accountDetailsSupplier(final String id) {
- return () -> getRestTemplate()
- .map(t -> this.getExamineeById(t, id))
- .getOrThrow();
+ public Result getExamineeAccountDetails(final String examineeUserId) {
+ return getRestTemplate().map(t -> this.getExamineeById(t, examineeUserId));
}
@Override
- protected Supplier getCourseChaptersSupplier(final String courseId) {
- return () -> {
- throw new UnsupportedOperationException("not available yet");
- };
+ public String getExamineeName(final String examineeUserId) {
+ return getExamineeAccountDetails(examineeUserId)
+ .map(ExamineeAccountDetails::getDisplayName)
+ .onError(error -> log.warn("Failed to request user-name for ID: {}", error.getMessage(), error))
+ .getOr(examineeUserId);
+ }
+
+ @Override
+ public Result getCourseChapters(final String courseId) {
+ return Result.ofError(new UnsupportedOperationException("not available yet"));
}
@Override
diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsLmsAPITemplateFactory.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsLmsAPITemplateFactory.java
index 549bf5a5..d0fc5385 100644
--- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsLmsAPITemplateFactory.java
+++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsLmsAPITemplateFactory.java
@@ -22,6 +22,7 @@ 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.LmsAPITemplateFactory;
+import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.LmsAPITemplateAdapter;
@Lazy
@Service
@@ -63,13 +64,17 @@ public class AnsLmsAPITemplateFactory implements LmsAPITemplateFactory {
@Override
public Result create(final APITemplateDataSupplier apiTemplateDataSupplier) {
return Result.tryCatch(() -> {
- return new AnsLmsAPITemplate(
+ final AnsLmsAPITemplate ansLmsAPITemplate = new AnsLmsAPITemplate(
this.clientHttpRequestFactoryService,
this.clientCredentialService,
apiTemplateDataSupplier,
+ this.cacheManager);
+ return new LmsAPITemplateAdapter(
this.asyncService,
this.environment,
- this.cacheManager);
+ apiTemplateDataSupplier,
+ ansLmsAPITemplate,
+ ansLmsAPITemplate);
});
}
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 fd4165b6..acce20b6 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
@@ -17,14 +17,12 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
-import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.CacheManager;
-import org.springframework.core.env.Environment;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
@@ -45,7 +43,6 @@ import com.fasterxml.jackson.core.type.TypeReference;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
-import ch.ethz.seb.sebserver.gbl.async.AsyncService;
import ch.ethz.seb.sebserver.gbl.model.exam.Chapters;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
@@ -57,12 +54,13 @@ 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.APITemplateDataSupplier;
+import ch.ethz.seb.sebserver.webservice.servicelayer.lms.CourseAccessAPI;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.AbstractCachedCourseAccess;
/** Implements the LmsAPITemplate for Open edX LMS Course API access.
*
* See also: https://course-catalog-api-guide.readthedocs.io */
-final class OpenEdxCourseAccess extends AbstractCachedCourseAccess {
+final class OpenEdxCourseAccess extends AbstractCachedCourseAccess implements CourseAccessAPI {
private static final Logger log = LoggerFactory.getLogger(OpenEdxCourseAccess.class);
@@ -84,11 +82,9 @@ final class OpenEdxCourseAccess extends AbstractCachedCourseAccess {
final JSONMapper jsonMapper,
final OpenEdxRestTemplateFactory openEdxRestTemplateFactory,
final WebserviceInfo webserviceInfo,
- final AsyncService asyncService,
- final Environment environment,
final CacheManager cacheManager) {
- super(asyncService, environment, cacheManager);
+ super(cacheManager);
this.jsonMapper = jsonMapper;
this.openEdxRestTemplateFactory = openEdxRestTemplateFactory;
this.webserviceInfo = webserviceInfo;
@@ -104,7 +100,8 @@ final class OpenEdxCourseAccess extends AbstractCachedCourseAccess {
return this.lmsSetupId;
}
- LmsSetupTestResult initAPIAccess() {
+ @Override
+ public LmsSetupTestResult testCourseAccessAPI() {
final LmsSetupTestResult attributesCheck = this.openEdxRestTemplateFactory.test();
if (!attributesCheck.isOk()) {
@@ -141,68 +138,18 @@ final class OpenEdxCourseAccess extends AbstractCachedCourseAccess {
}
@Override
- protected Supplier accountDetailsSupplier(final String examineeSessionId) {
- return () -> {
- try {
- final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup();
- final HttpHeaders httpHeaders = new HttpHeaders();
- final OAuth2RestTemplate template = getRestTemplate()
- .getOrThrow();
+ public Result> getQuizzes(final FilterMap filterMap) {
+ return getRestTemplate().map(this::collectAllQuizzes);
+ }
- final String externalStartURI = this.webserviceInfo
- .getLmsExternalAddressAlias(lmsSetup.lmsApiUrl);
-
- final String uri = (externalStartURI != null)
- ? externalStartURI + OPEN_EDX_DEFAULT_USER_PROFILE_ENDPOINT + examineeSessionId
- : lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_USER_PROFILE_ENDPOINT + examineeSessionId;
-
- final String responseJSON = template.exchange(
- uri,
- HttpMethod.GET,
- new HttpEntity<>(httpHeaders),
- String.class)
- .getBody();
-
- final EdxUserDetails[] userDetails = this.jsonMapper. readValue(
- responseJSON,
- new TypeReference() {
- });
-
- if (userDetails == null || userDetails.length <= 0) {
- throw new RuntimeException("No user details on Open edX API request");
- }
-
- final Map additionalAttributes = new HashMap<>();
- additionalAttributes.put("bio", userDetails[0].bio);
- additionalAttributes.put("country", userDetails[0].country);
- additionalAttributes.put("date_joined", userDetails[0].date_joined);
- additionalAttributes.put("gender", userDetails[0].gender);
- additionalAttributes.put("is_active", String.valueOf(userDetails[0].is_active));
- additionalAttributes.put("mailing_address", userDetails[0].mailing_address);
- additionalAttributes.put("secondary_email", userDetails[0].secondary_email);
-
- return new ExamineeAccountDetails(
- userDetails[0].username,
- userDetails[0].name,
- userDetails[0].username,
- userDetails[0].email,
- additionalAttributes);
- } catch (final Exception e) {
- throw new RuntimeException(e);
+ @Override
+ public Result getQuiz(final String id) {
+ return Result.tryCatch(() -> {
+ final QuizData fromCache = super.getFromCache(id);
+ if (fromCache != null) {
+ return fromCache;
}
- };
- }
- @Override
- protected Supplier> allQuizzesSupplier(final FilterMap filterMap) {
- return () -> getRestTemplate()
- .map(this::collectAllQuizzes)
- .getOrThrow();
- }
-
- @Override
- protected Supplier quizSupplier(final String id) {
- return () -> {
final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup();
final String externalStartURI = getExternalLMSServerAddress(lmsSetup);
final QuizData quizData = quizDataOf(
@@ -214,13 +161,13 @@ final class OpenEdxCourseAccess extends AbstractCachedCourseAccess {
super.putToCache(quizData);
}
return quizData;
- };
+ });
}
@Override
- protected Supplier> quizzesSupplier(final Set ids) {
+ public Result> getQuizzes(final Set ids) {
if (ids.size() == 1) {
- return () -> {
+ return Result.tryCatch(() -> {
final String id = ids.iterator().next();
@@ -239,17 +186,73 @@ final class OpenEdxCourseAccess extends AbstractCachedCourseAccess {
getRestTemplate().getOrThrow(),
id),
externalStartURI));
- };
+ });
} else {
- return () -> getRestTemplate()
- .map(template -> this.collectQuizzes(template, ids))
- .getOrThrow();
+ return getRestTemplate().map(template -> this.collectQuizzes(template, ids));
}
}
@Override
- protected Supplier getCourseChaptersSupplier(final String courseId) {
- return () -> {
+ public Result getExamineeAccountDetails(final String examineeUserId) {
+ return Result.tryCatch(() -> {
+
+ final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup();
+ final HttpHeaders httpHeaders = new HttpHeaders();
+ final OAuth2RestTemplate template = getRestTemplate()
+ .getOrThrow();
+
+ final String externalStartURI = this.webserviceInfo
+ .getLmsExternalAddressAlias(lmsSetup.lmsApiUrl);
+
+ final String uri = (externalStartURI != null)
+ ? externalStartURI + OPEN_EDX_DEFAULT_USER_PROFILE_ENDPOINT + examineeUserId
+ : lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_USER_PROFILE_ENDPOINT + examineeUserId;
+
+ final String responseJSON = template.exchange(
+ uri,
+ HttpMethod.GET,
+ new HttpEntity<>(httpHeaders),
+ String.class)
+ .getBody();
+
+ final EdxUserDetails[] userDetails = this.jsonMapper. readValue(
+ responseJSON,
+ new TypeReference() {
+ });
+
+ if (userDetails == null || userDetails.length <= 0) {
+ throw new RuntimeException("No user details on Open edX API request");
+ }
+
+ final Map additionalAttributes = new HashMap<>();
+ additionalAttributes.put("bio", userDetails[0].bio);
+ additionalAttributes.put("country", userDetails[0].country);
+ additionalAttributes.put("date_joined", userDetails[0].date_joined);
+ additionalAttributes.put("gender", userDetails[0].gender);
+ additionalAttributes.put("is_active", String.valueOf(userDetails[0].is_active));
+ additionalAttributes.put("mailing_address", userDetails[0].mailing_address);
+ additionalAttributes.put("secondary_email", userDetails[0].secondary_email);
+
+ return new ExamineeAccountDetails(
+ userDetails[0].username,
+ userDetails[0].name,
+ userDetails[0].username,
+ userDetails[0].email,
+ additionalAttributes);
+ });
+ }
+
+ @Override
+ public String getExamineeName(final String examineeUserId) {
+ return getExamineeAccountDetails(examineeUserId)
+ .map(ExamineeAccountDetails::getDisplayName)
+ .onError(error -> log.warn("Failed to request user-name for ID: {}", error.getMessage(), error))
+ .getOr(examineeUserId);
+ }
+
+ @Override
+ public Result getCourseChapters(final String courseId) {
+ return Result.tryCatch(() -> {
final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup();
final String uri =
@@ -262,7 +265,7 @@ final class OpenEdxCourseAccess extends AbstractCachedCourseAccess {
.filter(block -> OPEN_EDX_DEFAULT_BLOCKS_TYPE_CHAPTER.equals(block.type))
.map(block -> new Chapters.Chapter(block.display_name, block.block_id))
.collect(Collectors.toList()));
- };
+ });
}
public Result> getQuizzesFromCache(final Set ids) {
@@ -279,7 +282,7 @@ final class OpenEdxCourseAccess extends AbstractCachedCourseAccess {
});
if (!leftIds.isEmpty()) {
- result.addAll(super.protectedQuizzesRequest(leftIds).getOrThrow());
+ result.addAll(getQuizzes(leftIds).getOrThrow());
}
return result;
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 8ba22c7d..4e32bdcb 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
@@ -22,17 +22,20 @@ import org.springframework.web.client.HttpClientErrorException;
import com.fasterxml.jackson.core.JsonProcessingException;
import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
+import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.OpenEdxSEBRestriction;
+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.LmsType;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.util.Result;
+import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionAPI;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.NoSEBRestrictionException;
/** The open edX SEB course restriction API implementation.
*
* See also : https://seb-openedx.readthedocs.io/en/latest/ */
-public class OpenEdxCourseRestriction {
+public class OpenEdxCourseRestriction implements SEBRestrictionAPI {
private static final Logger log = LoggerFactory.getLogger(OpenEdxCourseRestriction.class);
@@ -54,7 +57,8 @@ public class OpenEdxCourseRestriction {
this.openEdxRestTemplateFactory = openEdxRestTemplateFactory;
}
- LmsSetupTestResult initAPIAccess() {
+ @Override
+ public LmsSetupTestResult testCourseRestrictionAPI() {
final LmsSetupTestResult attributesCheck = this.openEdxRestTemplateFactory.test();
if (!attributesCheck.isOk()) {
@@ -95,21 +99,22 @@ public class OpenEdxCourseRestriction {
}
if (log.isDebugEnabled()) {
- log.debug("Sucessfully checked SEB Open edX integration Plugin");
+ log.debug("Successfully checked SEB Open edX integration Plugin");
}
}
return LmsSetupTestResult.ofOkay(LmsType.OPEN_EDX);
}
- Result getSEBRestriction(final String courseId) {
+ @Override
+ public Result getSEBClientRestriction(final Exam exam) {
if (log.isDebugEnabled()) {
- log.debug("GET SEB Client restriction on course: {}", courseId);
+ log.debug("GET SEB Client restriction on exam: {}", exam);
}
+ final String courseId = exam.externalId;
final LmsSetup lmsSetup = this.openEdxRestTemplateFactory.apiTemplateDataSupplier.getLmsSetup();
-
return Result.tryCatch(() -> {
final String url = lmsSetup.lmsApiUrl + getSEBRestrictionUrl(courseId);
final HttpHeaders httpHeaders = new HttpHeaders();
@@ -127,7 +132,7 @@ public class OpenEdxCourseRestriction {
if (log.isDebugEnabled()) {
log.debug("Successfully GET SEB Client restriction on course: {}", courseId);
}
- return data;
+ return SEBRestriction.from(exam.id, data);
} catch (final HttpClientErrorException ce) {
if (ce.getStatusCode() == HttpStatus.NOT_FOUND || ce.getStatusCode() == HttpStatus.UNAUTHORIZED) {
throw new NoSEBRestrictionException(ce);
@@ -137,17 +142,18 @@ public class OpenEdxCourseRestriction {
});
}
- Result putSEBRestriction(
- final String courseId,
- final OpenEdxSEBRestriction restriction) {
+ @Override
+ public Result applySEBClientRestriction(
+ final String externalExamId,
+ final SEBRestriction sebRestrictionData) {
if (log.isDebugEnabled()) {
- log.debug("PUT SEB Client restriction on course: {} : {}", courseId, restriction);
+ log.debug("PUT SEB Client restriction on course: {} : {}", externalExamId, sebRestrictionData);
}
return Result.tryCatch(() -> {
final LmsSetup lmsSetup = this.openEdxRestTemplateFactory.apiTemplateDataSupplier.getLmsSetup();
- final String url = lmsSetup.lmsApiUrl + getSEBRestrictionUrl(courseId);
+ final String url = lmsSetup.lmsApiUrl + getSEBRestrictionUrl(externalExamId);
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");
@@ -157,24 +163,26 @@ public class OpenEdxCourseRestriction {
.exchange(
url,
HttpMethod.PUT,
- new HttpEntity<>(toJson(restriction), httpHeaders),
+ new HttpEntity<>(toJson(OpenEdxSEBRestriction.from(sebRestrictionData)), httpHeaders),
OpenEdxSEBRestriction.class)
.getBody();
if (log.isDebugEnabled()) {
- log.debug("Successfully PUT SEB Client restriction on course: {} : {}", courseId, body);
+ log.debug("Successfully PUT SEB Client restriction on course: {} : {}", externalExamId, body);
}
- return true;
+ return sebRestrictionData;
});
}
- Result deleteSEBRestriction(final String courseId) {
+ @Override
+ public Result releaseSEBClientRestriction(final Exam exam) {
if (log.isDebugEnabled()) {
- log.debug("DELETE SEB Client restriction on course: {}", courseId);
+ log.debug("DELETE SEB Client restriction on exam: {}", exam);
}
+ final String courseId = exam.externalId;
return Result.tryCatch(() -> {
final LmsSetup lmsSetup = this.openEdxRestTemplateFactory.apiTemplateDataSupplier.getLmsSetup();
final String url = lmsSetup.lmsApiUrl + getSEBRestrictionUrl(courseId);
@@ -193,7 +201,7 @@ public class OpenEdxCourseRestriction {
if (log.isDebugEnabled()) {
log.debug("Successfully PUT SEB Client restriction on course: {}", courseId);
}
- return true;
+ return exam;
} else {
throw new RuntimeException("Unexpected response for deletion: " + exchange);
}
@@ -201,74 +209,6 @@ public class OpenEdxCourseRestriction {
}
-// private BooleanSupplier pushSEBRestrictionFunction(
-// final OpenEdxSEBRestriction restriction,
-// final String courseId) {
-//
-// 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");
-// 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 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