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 exchange = this.restTemplate.exchange( -// url, -// HttpMethod.DELETE, -// new HttpEntity<>(httpHeaders), -// Object.class); -// -// if (exchange.getStatusCode() == HttpStatus.NO_CONTENT) { -// if (log.isDebugEnabled()) { -// log.debug("Successfully PUT SEB Client restriction on course: {}", courseId); -// } -// } else { -// log.error("Unexpected response for deletion: {}", exchange); -// return false; -// } -// -// return true; -// }; -// } - -// private Result handleSEBRestriction(final BooleanSupplier task) { -// return getRestTemplate() -// .map(restTemplate -> { -// try { -// return task.getAsBoolean(); -// } catch (final HttpClientErrorException ce) { -// if (ce.getStatusCode() == HttpStatus.UNAUTHORIZED) { -// throw new APIMessageException(APIMessage.ErrorMessage.UNAUTHORIZED.of(ce.getMessage() -// + " Unable to get access for API. Please check the corresponding LMS Setup ")); -// } -// throw ce; -// } catch (final Exception e) { -// throw new RuntimeException("Unexpected: ", e); -// } -// }); -// } - private String getSEBRestrictionUrl(final String courseId) { return String.format(OPEN_EDX_DEFAULT_COURSE_RESTRICTION_API_PATH, courseId); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxLmsAPITemplate.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxLmsAPITemplate.java deleted file mode 100644 index 8975f97a..00000000 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxLmsAPITemplate.java +++ /dev/null @@ -1,159 +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.edx; - -import java.util.Collection; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; - -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.Exam; -import ch.ethz.seb.sebserver.gbl.model.exam.OpenEdxSEBRestriction; -import ch.ethz.seb.sebserver.gbl.model.exam.QuizData; -import ch.ethz.seb.sebserver.gbl.model.exam.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.LmsAPIService; -import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate; - -/** The OpenEdxLmsAPITemplate is separated into two parts: - * - OpenEdxCourseAccess implements the course access API - * - OpenEdxCourseRestriction implements the SEB restriction API - * - Both uses the OpenEdxRestTemplateFactory to create a spring based RestTemplate to access the LMS API */ -final class OpenEdxLmsAPITemplate implements LmsAPITemplate { - - private static final Logger log = LoggerFactory.getLogger(OpenEdxLmsAPITemplate.class); - - private final OpenEdxCourseAccess openEdxCourseAccess; - private final OpenEdxCourseRestriction openEdxCourseRestriction; - - OpenEdxLmsAPITemplate( - final OpenEdxCourseAccess openEdxCourseAccess, - final OpenEdxCourseRestriction openEdxCourseRestriction) { - - this.openEdxCourseAccess = openEdxCourseAccess; - this.openEdxCourseRestriction = openEdxCourseRestriction; - } - - @Override - public LmsType getType() { - return LmsType.OPEN_EDX; - } - - @Override - public LmsSetup lmsSetup() { - return this.openEdxCourseAccess - .getApiTemplateDataSupplier() - .getLmsSetup(); - } - - @Override - public LmsSetupTestResult testCourseAccessAPI() { - return this.openEdxCourseAccess.initAPIAccess(); - } - - @Override - public LmsSetupTestResult testCourseRestrictionAPI() { - return this.openEdxCourseRestriction.initAPIAccess(); - } - - @Override - public Result> getQuizzes(final FilterMap filterMap) { - return this.openEdxCourseAccess - .protectedQuizzesRequest(filterMap) - .map(quizzes -> quizzes.stream() - .filter(LmsAPIService.quizFilterPredicate(filterMap)) - .collect(Collectors.toList())); - } - - @Override - public Result getQuiz(final String id) { - final QuizData quizFromCache = this.openEdxCourseAccess.getQuizFromCache(id); - if (quizFromCache != null) { - return Result.of(quizFromCache); - } - - return this.openEdxCourseAccess.protectedQuizRequest(id); - } - - @Override - public Result> getQuizzes(final Set ids) { - return this.openEdxCourseAccess.getQuizzesFromCache(ids); - } - - @Override - public void clearCache() { - this.openEdxCourseAccess.clearCache(); - } - - @Override - public Result getCourseChapters(final String courseId) { - return Result.tryCatch(() -> this.openEdxCourseAccess - .getCourseChaptersSupplier(courseId) - .get()); - } - - @Override - public Result getExamineeAccountDetails(final String examineeSessionId) { - return this.openEdxCourseAccess.getExamineeAccountDetails(examineeSessionId); - } - - @Override - public String getExamineeName(final String examineeSessionId) { - return this.openEdxCourseAccess.getExamineeName(examineeSessionId); - } - - @Override - public Result getSEBClientRestriction(final Exam exam) { - if (log.isDebugEnabled()) { - log.debug("Get SEB Client restriction for Exam: {}", exam); - } - - return this.openEdxCourseRestriction - .getSEBRestriction(exam.externalId) - .map(restriction -> SEBRestriction.from(exam.id, restriction)); - } - - @Override - public Result applySEBClientRestriction( - final String externalExamId, - final SEBRestriction sebRestrictionData) { - - if (log.isDebugEnabled()) { - log.debug("Apply SEB Client restriction: {}", sebRestrictionData); - } - - return this.openEdxCourseRestriction - .putSEBRestriction( - externalExamId, - OpenEdxSEBRestriction.from(sebRestrictionData)) - .map(result -> sebRestrictionData); - } - - @Override - public Result releaseSEBClientRestriction(final Exam exam) { - - if (log.isDebugEnabled()) { - log.debug("Release SEB Client restriction for Exam: {}", exam); - } - - return this.openEdxCourseRestriction - .deleteSEBRestriction(exam.externalId) - .map(result -> exam); - } - -} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxLmsAPITemplateFactory.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxLmsAPITemplateFactory.java index a3609f16..20b3ea92 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxLmsAPITemplateFactory.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxLmsAPITemplateFactory.java @@ -27,6 +27,7 @@ import ch.ethz.seb.sebserver.webservice.WebserviceInfo; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplateFactory; +import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.LmsAPITemplateAdapter; @Lazy @Service @@ -87,8 +88,6 @@ public class OpenEdxLmsAPITemplateFactory implements LmsAPITemplateFactory { this.jsonMapper, openEdxRestTemplateFactory, this.webserviceInfo, - this.asyncService, - this.environment, this.cacheManager); final OpenEdxCourseRestriction openEdxCourseRestriction = new OpenEdxCourseRestriction( @@ -96,7 +95,10 @@ public class OpenEdxLmsAPITemplateFactory implements LmsAPITemplateFactory { openEdxRestTemplateFactory, this.restrictionAPIPushCount); - return new OpenEdxLmsAPITemplate( + return new LmsAPITemplateAdapter( + this.asyncService, + this.environment, + apiTemplateDataSupplier, openEdxCourseAccess, openEdxCourseRestriction); }); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockupLmsAPITemplate.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockCourseAccessAPI.java similarity index 67% rename from src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockupLmsAPITemplate.java rename to src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockCourseAccessAPI.java index ec71bb8e..90fbc680 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockupLmsAPITemplate.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockCourseAccessAPI.java @@ -1,323 +1,224 @@ -/* - * 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.mockup; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Set; -import java.util.function.Supplier; -import java.util.stream.Collectors; - -import org.apache.commons.lang3.StringUtils; -import org.joda.time.DateTime; -import org.joda.time.DateTimeZone; -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.api.APIMessage; -import ch.ethz.seb.sebserver.gbl.async.AsyncService; -import ch.ethz.seb.sebserver.gbl.client.ClientCredentials; -import ch.ethz.seb.sebserver.gbl.model.Domain.LMS_SETUP; -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.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.LmsAPIService; -import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate; -import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.AbstractCourseAccess; -import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.NoSEBRestrictionException; - -public class MockupLmsAPITemplate implements LmsAPITemplate { - - private static final Logger log = LoggerFactory.getLogger(MockupLmsAPITemplate.class); - - private final Collection mockups; - private final WebserviceInfo webserviceInfo; - private final APITemplateDataSupplier apiTemplateDataSupplier; - - private final AbstractCourseAccess abstractCourseAccess; - - MockupLmsAPITemplate( - final AsyncService asyncService, - final Environment environment, - final APITemplateDataSupplier apiTemplateDataSupplier, - final WebserviceInfo webserviceInfo) { - - this.apiTemplateDataSupplier = apiTemplateDataSupplier; - this.webserviceInfo = webserviceInfo; - this.mockups = new ArrayList<>(); - - this.abstractCourseAccess = new AbstractCourseAccess(asyncService, environment) { - - @Override - protected Supplier accountDetailsSupplier(final String examineeSessionId) { - return () -> MockupLmsAPITemplate.this - .getExamineeAccountDetails_protected(examineeSessionId) - .getOrThrow(); - } - - @Override - protected Supplier> allQuizzesSupplier(final FilterMap filterMap) { - return () -> MockupLmsAPITemplate.this.getQuizzes_protected(filterMap).getOrThrow(); - } - - @Override - protected Supplier> quizzesSupplier(final Set ids) { - return () -> MockupLmsAPITemplate.this.getQuizzes_protected(ids).getOrThrow(); - } - - @Override - protected Supplier quizSupplier(final String id) { - return () -> MockupLmsAPITemplate.this.getQuiz_protected(id).getOrThrow(); - } - - @Override - protected Supplier getCourseChaptersSupplier(final String courseId) { - throw new UnsupportedOperationException("Course Chapter feature not supported"); - } - - }; - - final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup(); - final Long lmsSetupId = lmsSetup.id; - final Long institutionId = lmsSetup.getInstitutionId(); - final LmsType lmsType = lmsSetup.getLmsType(); - - this.mockups.add(new QuizData( - "quiz1", institutionId, lmsSetupId, lmsType, "Demo Quiz 1 (MOCKUP)", "

Demo Quiz Mockup

", - "2020-01-01T09:00:00Z", null, "http://lms.mockup.com/api/")); - this.mockups.add(new QuizData( - "quiz2", institutionId, lmsSetupId, lmsType, "Demo Quiz 2 (MOCKUP)", "

Demo Quiz Mockup

", - "2020-01-01T09:00:00Z", "2025-01-01T09:00:00Z", "http://lms.mockup.com/api/")); - this.mockups.add(new QuizData( - "quiz3", institutionId, lmsSetupId, lmsType, "Demo Quiz 3 (MOCKUP)", "

Demo Quiz Mockup

", - "2018-07-30T09:00:00Z", "2018-08-01T00:00:00Z", "http://lms.mockup.com/api/")); - this.mockups.add(new QuizData( - "quiz4", institutionId, lmsSetupId, lmsType, "Demo Quiz 4 (MOCKUP)", "

Demo Quiz Mockup

", - "2018-01-01T00:00:00Z", "2025-01-01T00:00:00Z", "http://lms.mockup.com/api/")); - this.mockups.add(new QuizData( - "quiz5", institutionId, lmsSetupId, lmsType, "Demo Quiz 5 (MOCKUP)", "

Demo Quiz Mockup

", - "2018-01-01T09:00:00Z", "2025-01-01T09:00:00Z", "http://lms.mockup.com/api/")); - this.mockups.add(new QuizData( - "quiz6", institutionId, lmsSetupId, lmsType, "Demo Quiz 6 (MOCKUP)", "

Demo Quiz Mockup

", - "2019-01-01T09:00:00Z", "2025-01-01T09:00:00Z", "http://lms.mockup.com/api/")); - this.mockups.add(new QuizData( - "quiz7", institutionId, lmsSetupId, lmsType, "Demo Quiz 7 (MOCKUP)", "

Demo Quiz Mockup

", - "2018-01-01T09:00:00Z", "2025-01-01T09:00:00Z", "http://lms.mockup.com/api/")); - - this.mockups.add(new QuizData( - "quiz10", institutionId, lmsSetupId, lmsType, "Demo Quiz 10 (MOCKUP)", - "Starts in a minute and ends after five minutes", - DateTime.now(DateTimeZone.UTC).plus(Constants.MINUTE_IN_MILLIS) - .toString(Constants.DEFAULT_DATE_TIME_FORMAT), - DateTime.now(DateTimeZone.UTC).plus(6 * Constants.MINUTE_IN_MILLIS) - .toString(Constants.DEFAULT_DATE_TIME_FORMAT), - "http://lms.mockup.com/api/")); - } - - @Override - public LmsType getType() { - return LmsType.MOCKUP; - } - - @Override - public LmsSetup lmsSetup() { - return this.apiTemplateDataSupplier.getLmsSetup(); - } - - private List checkAttributes() { - final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup(); - final ClientCredentials lmsClientCredentials = this.apiTemplateDataSupplier.getLmsClientCredentials(); - final List missingAttrs = new ArrayList<>(); - if (StringUtils.isBlank(lmsSetup.lmsApiUrl)) { - missingAttrs.add(APIMessage.fieldValidationError( - LMS_SETUP.ATTR_LMS_URL, - "lmsSetup:lmsUrl:notNull")); - } - if (!lmsClientCredentials.hasClientId()) { - missingAttrs.add(APIMessage.fieldValidationError( - LMS_SETUP.ATTR_LMS_CLIENTNAME, - "lmsSetup:lmsClientname:notNull")); - } - if (!lmsClientCredentials.hasSecret()) { - missingAttrs.add(APIMessage.fieldValidationError( - LMS_SETUP.ATTR_LMS_CLIENTSECRET, - "lmsSetup:lmsClientsecret:notNull")); - } - return missingAttrs; - } - - @Override - public LmsSetupTestResult testCourseAccessAPI() { - log.info("Test Lms Binding for Mockup and LmsSetup: {}", this.apiTemplateDataSupplier.getLmsSetup()); - - final List missingAttrs = checkAttributes(); - - if (!missingAttrs.isEmpty()) { - return LmsSetupTestResult.ofMissingAttributes(LmsType.MOCKUP, missingAttrs); - } - - if (authenticate()) { - return LmsSetupTestResult.ofOkay(LmsType.MOCKUP); - } else { - return LmsSetupTestResult.ofTokenRequestError(LmsType.MOCKUP, "Illegal access"); - } - } - - @Override - public LmsSetupTestResult testCourseRestrictionAPI() { - return LmsSetupTestResult.ofQuizRestrictionAPIError(LmsType.MOCKUP, "unsupported"); - } - - @Override - public Result> getQuizzes(final FilterMap filterMap) { - return this.abstractCourseAccess.protectedQuizzesRequest(filterMap); - } - - private Result> getQuizzes_protected(final FilterMap filterMap) { - return Result.tryCatch(() -> { - if (!authenticate()) { - throw new IllegalArgumentException("Wrong clientId or secret"); - } - - return this.mockups - .stream() - .map(this::getExternalAddressAlias) - .filter(LmsAPIService.quizFilterPredicate(filterMap)) - .collect(Collectors.toList()); - }); - } - - @Override - public Result getQuiz(final String id) { - return this.abstractCourseAccess.protectedQuizRequest(id); - } - - private Result getQuiz_protected(final String id) { - return Result.of(this.mockups - .stream() - .filter(q -> id.equals(q.id)) - .findFirst() - .get()); - } - - @Override - public Result> getQuizzes(final Set ids) { - return this.abstractCourseAccess.protectedQuizzesRequest(ids); - } - - private Result> getQuizzes_protected(final Set ids) { - - return Result.tryCatch(() -> { - if (!authenticate()) { - throw new IllegalArgumentException("Wrong clientId or secret"); - } - - return this.mockups - .stream() - .map(this::getExternalAddressAlias) - .filter(mock -> ids.contains(mock.id)) - .collect(Collectors.toList()); - }); - } - - @Override - public void clearCache() { - - } - - @Override - public Result getCourseChapters(final String courseId) { - return this.abstractCourseAccess.getCourseChapters(courseId); - } - - @Override - public Result getExamineeAccountDetails(final String examineeSessionId) { - return this.abstractCourseAccess.getExamineeAccountDetails(examineeSessionId); - } - - private Result getExamineeAccountDetails_protected(final String examineeSessionId) { - return Result.ofError(new UnsupportedOperationException()); - } - - @Override - public String getExamineeName(final String examineeSessionId) { - return "--" + " (" + examineeSessionId + ")"; - } - - @Override - public Result getSEBClientRestriction(final Exam exam) { - log.info("Apply SEB Client restriction for Exam: {}", exam); - return Result.ofError(new NoSEBRestrictionException()); - } - - @Override - public Result applySEBClientRestriction( - final String externalExamId, - final SEBRestriction sebRestrictionData) { - - log.info("Apply SEB Client restriction: {}", sebRestrictionData); - return Result.of(sebRestrictionData); - } - - @Override - public Result releaseSEBClientRestriction(final Exam exam) { - log.info("Release SEB Client restriction for Exam: {}", exam); - return Result.of(exam); - } - - private QuizData getExternalAddressAlias(final QuizData quizData) { - final String externalAddressAlias = this.webserviceInfo.getLmsExternalAddressAlias("lms.mockup.com"); - if (StringUtils.isNoneBlank(externalAddressAlias)) { - try { - - final String _externalStartURI = - this.webserviceInfo.getHttpScheme() + - "://" + externalAddressAlias + "/api/"; - - return new QuizData( - quizData.id, quizData.institutionId, quizData.lmsSetupId, quizData.lmsType, - quizData.name, quizData.description, quizData.startTime, - quizData.endTime, _externalStartURI, quizData.additionalAttributes); - } catch (final Exception e) { - log.error("Failed to create external address from alias: ", e); - return quizData; - } - } else { - return quizData; - } - } - - private boolean authenticate() { - try { - - final CharSequence plainClientId = this.apiTemplateDataSupplier.getLmsClientCredentials().clientId; - if (plainClientId == null || plainClientId.length() <= 0) { - throw new IllegalAccessException("Wrong client credential"); - } - - return true; - } catch (final Exception e) { - log.info("Authentication failed: ", e); - return false; - } - } - -} +/* + * 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.mockup; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.StringUtils; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; + +import ch.ethz.seb.sebserver.gbl.Constants; +import ch.ethz.seb.sebserver.gbl.api.APIMessage; +import ch.ethz.seb.sebserver.gbl.client.ClientCredentials; +import ch.ethz.seb.sebserver.gbl.model.Domain.LMS_SETUP; +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.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.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.LmsAPIService; + +public class MockCourseAccessAPI implements CourseAccessAPI { + + private final Collection mockups; + private final WebserviceInfo webserviceInfo; + private final APITemplateDataSupplier apiTemplateDataSupplier; + + public MockCourseAccessAPI( + final APITemplateDataSupplier apiTemplateDataSupplier, + final WebserviceInfo webserviceInfo) { + + this.apiTemplateDataSupplier = apiTemplateDataSupplier; + this.webserviceInfo = webserviceInfo; + this.mockups = new ArrayList<>(); + + final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup(); + final Long lmsSetupId = lmsSetup.id; + final Long institutionId = lmsSetup.getInstitutionId(); + final LmsType lmsType = lmsSetup.getLmsType(); + + this.mockups.add(new QuizData( + "quiz1", institutionId, lmsSetupId, lmsType, "Demo Quiz 1 (MOCKUP)", "

Demo Quiz Mockup

", + "2020-01-01T09:00:00Z", null, "http://lms.mockup.com/api/")); + this.mockups.add(new QuizData( + "quiz2", institutionId, lmsSetupId, lmsType, "Demo Quiz 2 (MOCKUP)", "

Demo Quiz Mockup

", + "2020-01-01T09:00:00Z", "2025-01-01T09:00:00Z", "http://lms.mockup.com/api/")); + this.mockups.add(new QuizData( + "quiz3", institutionId, lmsSetupId, lmsType, "Demo Quiz 3 (MOCKUP)", "

Demo Quiz Mockup

", + "2018-07-30T09:00:00Z", "2018-08-01T00:00:00Z", "http://lms.mockup.com/api/")); + this.mockups.add(new QuizData( + "quiz4", institutionId, lmsSetupId, lmsType, "Demo Quiz 4 (MOCKUP)", "

Demo Quiz Mockup

", + "2018-01-01T00:00:00Z", "2025-01-01T00:00:00Z", "http://lms.mockup.com/api/")); + this.mockups.add(new QuizData( + "quiz5", institutionId, lmsSetupId, lmsType, "Demo Quiz 5 (MOCKUP)", "

Demo Quiz Mockup

", + "2018-01-01T09:00:00Z", "2025-01-01T09:00:00Z", "http://lms.mockup.com/api/")); + this.mockups.add(new QuizData( + "quiz6", institutionId, lmsSetupId, lmsType, "Demo Quiz 6 (MOCKUP)", "

Demo Quiz Mockup

", + "2019-01-01T09:00:00Z", "2025-01-01T09:00:00Z", "http://lms.mockup.com/api/")); + this.mockups.add(new QuizData( + "quiz7", institutionId, lmsSetupId, lmsType, "Demo Quiz 7 (MOCKUP)", "

Demo Quiz Mockup

", + "2018-01-01T09:00:00Z", "2025-01-01T09:00:00Z", "http://lms.mockup.com/api/")); + + this.mockups.add(new QuizData( + "quiz10", institutionId, lmsSetupId, lmsType, "Demo Quiz 10 (MOCKUP)", + "Starts in a minute and ends after five minutes", + DateTime.now(DateTimeZone.UTC).plus(Constants.MINUTE_IN_MILLIS) + .toString(Constants.DEFAULT_DATE_TIME_FORMAT), + DateTime.now(DateTimeZone.UTC).plus(6 * Constants.MINUTE_IN_MILLIS) + .toString(Constants.DEFAULT_DATE_TIME_FORMAT), + "http://lms.mockup.com/api/")); + } + + @Override + public LmsSetupTestResult testCourseAccessAPI() { + log.info("Test Lms Binding for Mockup and LmsSetup: {}", this.apiTemplateDataSupplier.getLmsSetup()); + + final List missingAttrs = checkAttributes(); + + if (!missingAttrs.isEmpty()) { + return LmsSetupTestResult.ofMissingAttributes(LmsType.MOCKUP, missingAttrs); + } + + if (authenticate()) { + return LmsSetupTestResult.ofOkay(LmsType.MOCKUP); + } else { + return LmsSetupTestResult.ofTokenRequestError(LmsType.MOCKUP, "Illegal access"); + } + } + + private List checkAttributes() { + final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup(); + final ClientCredentials lmsClientCredentials = this.apiTemplateDataSupplier.getLmsClientCredentials(); + final List missingAttrs = new ArrayList<>(); + if (StringUtils.isBlank(lmsSetup.lmsApiUrl)) { + missingAttrs.add(APIMessage.fieldValidationError( + LMS_SETUP.ATTR_LMS_URL, + "lmsSetup:lmsUrl:notNull")); + } + if (!lmsClientCredentials.hasClientId()) { + missingAttrs.add(APIMessage.fieldValidationError( + LMS_SETUP.ATTR_LMS_CLIENTNAME, + "lmsSetup:lmsClientname:notNull")); + } + if (!lmsClientCredentials.hasSecret()) { + missingAttrs.add(APIMessage.fieldValidationError( + LMS_SETUP.ATTR_LMS_CLIENTSECRET, + "lmsSetup:lmsClientsecret:notNull")); + } + return missingAttrs; + } + + @Override + public Result> getQuizzes(final FilterMap filterMap) { + return Result.tryCatch(() -> { + if (!authenticate()) { + throw new IllegalArgumentException("Wrong clientId or secret"); + } + + return this.mockups + .stream() + .map(this::getExternalAddressAlias) + .filter(LmsAPIService.quizFilterPredicate(filterMap)) + .collect(Collectors.toList()); + }); + } + + @Override + public Result> getQuizzes(final Set ids) { + return Result.tryCatch(() -> { + if (!authenticate()) { + throw new IllegalArgumentException("Wrong clientId or secret"); + } + + return this.mockups + .stream() + .map(this::getExternalAddressAlias) + .filter(mock -> ids.contains(mock.id)) + .collect(Collectors.toList()); + }); + } + + @Override + public Result getQuiz(final String id) { + return Result.of(this.mockups + .stream() + .filter(q -> id.equals(q.id)) + .findFirst() + .get()); + } + + @Override + public void clearCourseCache() { + // No cache here + } + + @Override + public Result getExamineeAccountDetails(final String examineeUserId) { + return Result.ofError(new UnsupportedOperationException()); + } + + @Override + public String getExamineeName(final String examineeUserId) { + return "--" + " (" + examineeUserId + ")"; + } + + @Override + public Result getCourseChapters(final String courseId) { + return Result.ofError(new UnsupportedOperationException("Course Chapter feature not supported")); + } + + private boolean authenticate() { + try { + + final CharSequence plainClientId = this.apiTemplateDataSupplier.getLmsClientCredentials().clientId; + if (plainClientId == null || plainClientId.length() <= 0) { + throw new IllegalAccessException("Wrong client credential"); + } + + return true; + } catch (final Exception e) { + log.info("Authentication failed: ", e); + return false; + } + } + + private QuizData getExternalAddressAlias(final QuizData quizData) { + final String externalAddressAlias = this.webserviceInfo.getLmsExternalAddressAlias("lms.mockup.com"); + if (StringUtils.isNoneBlank(externalAddressAlias)) { + try { + + final String _externalStartURI = + this.webserviceInfo.getHttpScheme() + + "://" + externalAddressAlias + "/api/"; + + return new QuizData( + quizData.id, quizData.institutionId, quizData.lmsSetupId, quizData.lmsType, + quizData.name, quizData.description, quizData.startTime, + quizData.endTime, _externalStartURI, quizData.additionalAttributes); + } catch (final Exception e) { + log.error("Failed to create external address from alias: ", e); + return quizData; + } + } else { + return quizData; + } + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockLmsAPITemplateFactory.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockLmsAPITemplateFactory.java index 032e7d6a..911b1ab2 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockLmsAPITemplateFactory.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockLmsAPITemplateFactory.java @@ -20,6 +20,7 @@ import ch.ethz.seb.sebserver.webservice.WebserviceInfo; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplateFactory; +import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.LmsAPITemplateAdapter; @Lazy @Service @@ -47,11 +48,19 @@ public class MockLmsAPITemplateFactory implements LmsAPITemplateFactory { @Override public Result create(final APITemplateDataSupplier apiTemplateDataSupplier) { - return Result.tryCatch(() -> new MockupLmsAPITemplate( + + final MockCourseAccessAPI mockCourseAccessAPI = new MockCourseAccessAPI( + apiTemplateDataSupplier, + this.webserviceInfo); + + final MockSEBRestrictionAPI mockSEBRestrictionAPI = new MockSEBRestrictionAPI(); + + return Result.tryCatch(() -> new LmsAPITemplateAdapter( this.asyncService, this.environment, apiTemplateDataSupplier, - this.webserviceInfo)); + mockCourseAccessAPI, + mockSEBRestrictionAPI)); } } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockSEBRestrictionAPI.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockSEBRestrictionAPI.java new file mode 100644 index 00000000..5cfe2360 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockSEBRestrictionAPI.java @@ -0,0 +1,52 @@ +/* + * 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.mockup; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +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.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; + +public class MockSEBRestrictionAPI implements SEBRestrictionAPI { + + private static final Logger log = LoggerFactory.getLogger(MockSEBRestrictionAPI.class); + + @Override + public LmsSetupTestResult testCourseRestrictionAPI() { + return LmsSetupTestResult.ofQuizRestrictionAPIError(LmsType.MOCKUP, "unsupported"); + } + + @Override + public Result getSEBClientRestriction(final Exam exam) { + log.info("Apply SEB Client restriction for Exam: {}", exam); + return Result.ofError(new NoSEBRestrictionException()); + } + + @Override + public Result applySEBClientRestriction( + final String externalExamId, + final SEBRestriction sebRestrictionData) { + + log.info("Apply SEB Client restriction: {}", sebRestrictionData); + return Result.of(sebRestrictionData); + } + + @Override + public Result releaseSEBClientRestriction(final Exam exam) { + log.info("Release SEB Client restriction for Exam: {}", exam); + return Result.of(exam); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleCourseAccess.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleCourseAccess.java index 54461dfe..8f034416 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleCourseAccess.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleCourseAccess.java @@ -16,7 +16,6 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Function; -import java.util.function.Supplier; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -37,9 +36,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.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.institution.LmsSetup; @@ -50,7 +46,7 @@ import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Utils; 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.impl.AbstractCourseAccess; +import ch.ethz.seb.sebserver.webservice.servicelayer.lms.CourseAccessAPI; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleCourseDataAsyncLoader.CourseDataShort; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleCourseDataAsyncLoader.CourseQuizShort; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory.MoodleAPIRestTemplate; @@ -69,7 +65,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestT * background task if needed and return immediately to do not block the request. * The planed Moodle integration on moodle side also defines an improved course access API. This will * possibly make this synchronous fetch strategy obsolete in the future. */ -public class MoodleCourseAccess extends AbstractCourseAccess { +public class MoodleCourseAccess implements CourseAccessAPI { private static final long INITIAL_WAIT_TIME = 3 * Constants.SECOND_IN_MILLIS; @@ -94,7 +90,6 @@ public class MoodleCourseAccess extends AbstractCourseAccess { private final JSONMapper jsonMapper; private final MoodleRestTemplateFactory moodleRestTemplateFactory; private final MoodleCourseDataAsyncLoader moodleCourseDataAsyncLoader; - private final CircuitBreaker> allQuizzesRequest; private final boolean prependShortCourseName; private MoodleAPIRestTemplate restTemplate; @@ -103,28 +98,12 @@ public class MoodleCourseAccess extends AbstractCourseAccess { final JSONMapper jsonMapper, final MoodleRestTemplateFactory moodleRestTemplateFactory, final MoodleCourseDataAsyncLoader moodleCourseDataAsyncLoader, - final AsyncService asyncService, final Environment environment) { - super(asyncService, environment); this.jsonMapper = jsonMapper; this.moodleCourseDataAsyncLoader = moodleCourseDataAsyncLoader; this.moodleRestTemplateFactory = moodleRestTemplateFactory; - this.allQuizzesRequest = asyncService.createCircuitBreaker( - environment.getProperty( - "sebserver.webservice.circuitbreaker.allQuizzesRequest.attempts", - Integer.class, - 3), - environment.getProperty( - "sebserver.webservice.circuitbreaker.allQuizzesRequest.blockingTime", - Long.class, - Constants.MINUTE_IN_MILLIS), - environment.getProperty( - "sebserver.webservice.circuitbreaker.allQuizzesRequest.timeToRecover", - Long.class, - Constants.MINUTE_IN_MILLIS)); - this.prependShortCourseName = BooleanUtils.toBoolean(environment.getProperty( "sebserver.webservice.lms.moodle.prependShortCourseName", Constants.TRUE_STRING)); @@ -135,67 +114,7 @@ public class MoodleCourseAccess extends AbstractCourseAccess { } @Override - protected Supplier accountDetailsSupplier(final String examineeSessionId) { - return () -> { - try { - final MoodleAPIRestTemplate template = getRestTemplate() - .getOrThrow(); - - final MultiValueMap queryAttributes = new LinkedMultiValueMap<>(); - queryAttributes.add("field", "id"); - queryAttributes.add("values[0]", examineeSessionId); - - final String userDetailsJSON = template.callMoodleAPIFunction( - MOODLE_USER_PROFILE_API_FUNCTION_NAME, - queryAttributes); - - if (checkAccessDeniedError(userDetailsJSON)) { - final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup(); - log.error("Get access denied error from Moodle: {} for API call: {}, response: {}", - lmsSetup, - MOODLE_USER_PROFILE_API_FUNCTION_NAME, - Utils.truncateText(userDetailsJSON, 2000)); - throw new RuntimeException("No user details on Moodle API request (access-denied)"); - } - - final MoodleUserDetails[] userDetails = this.jsonMapper. readValue( - userDetailsJSON, - new TypeReference() { - }); - - if (userDetails == null || userDetails.length <= 0) { - throw new RuntimeException("No user details on Moodle API request"); - } - - final Map additionalAttributes = new HashMap<>(); - additionalAttributes.put("firstname", userDetails[0].firstname); - additionalAttributes.put("lastname", userDetails[0].lastname); - additionalAttributes.put("department", userDetails[0].department); - additionalAttributes.put("firstaccess", String.valueOf(userDetails[0].firstaccess)); - additionalAttributes.put("lastaccess", String.valueOf(userDetails[0].lastaccess)); - additionalAttributes.put("auth", userDetails[0].auth); - additionalAttributes.put("suspended", String.valueOf(userDetails[0].suspended)); - additionalAttributes.put("confirmed", String.valueOf(userDetails[0].confirmed)); - additionalAttributes.put("lang", userDetails[0].lang); - additionalAttributes.put("theme", userDetails[0].theme); - additionalAttributes.put("timezone", userDetails[0].timezone); - additionalAttributes.put("description", userDetails[0].description); - additionalAttributes.put("mailformat", String.valueOf(userDetails[0].mailformat)); - additionalAttributes.put("descriptionformat", String.valueOf(userDetails[0].descriptionformat)); - return new ExamineeAccountDetails( - userDetails[0].id, - userDetails[0].fullname, - userDetails[0].username, - userDetails[0].email, - additionalAttributes); - } catch (final Exception e) { - throw new RuntimeException(e); - } - }; - } - - LmsSetupTestResult initAPIAccess() { - + public LmsSetupTestResult testCourseAccessAPI() { final LmsSetupTestResult attributesCheck = this.moodleRestTemplateFactory.test(); if (!attributesCheck.isOk()) { return attributesCheck; @@ -223,7 +142,45 @@ public class MoodleCourseAccess extends AbstractCourseAccess { return LmsSetupTestResult.ofOkay(LmsType.MOODLE); } - public Result getQuizFromCache(final String id) { + @Override + public Result> getQuizzes(final FilterMap filterMap) { + return Result.tryCatch(() -> getRestTemplate() + .map(template -> collectAllQuizzes(template, filterMap)) + .getOr(Collections.emptyList())); + } + + @Override + public Result> getQuizzes(final Set ids) { + return Result.tryCatch(() -> { + final List cached = getCached(); + final List available = (cached != null) + ? cached + : Collections.emptyList(); + + final Map quizMapping = available + .stream() + .collect(Collectors.toMap(q -> q.id, Function.identity())); + + if (!quizMapping.keySet().containsAll(ids)) { + + final Map collect = getRestTemplate() + .map(template -> getQuizzesForIds(template, ids)) + .getOrElse(() -> Collections.emptyList()) + .stream() + .collect(Collectors.toMap(qd -> qd.id, Function.identity())); + if (collect != null) { + quizMapping.clear(); + quizMapping.putAll(collect); + } + } + + return quizMapping.values(); + + }); + } + + @Override + public Result getQuiz(final String id) { return Result.tryCatch(() -> { final Map cachedCourseData = this.moodleCourseDataAsyncLoader @@ -255,76 +212,93 @@ public class MoodleCourseAccess extends AbstractCourseAccess { } // get from LMS in protected request - return super.protectedQuizRequest(id).getOrThrow(); - }); - } - - public Result> getQuizzesFromCache(final Set ids) { - return Result.tryCatch(() -> { - final List cached = getCached(); - final List available = (cached != null) - ? cached - : Collections.emptyList(); - - final Map quizMapping = available - .stream() - .collect(Collectors.toMap(q -> q.id, Function.identity())); - - if (!quizMapping.keySet().containsAll(ids)) { - - final Map collect = super.quizzesRequest - .protectedRun(quizzesSupplier(ids)) - .onError(error -> log.error("Failed to get quizzes by ids: ", error)) - .getOrElse(() -> Collections.emptyList()) - .stream() - .collect(Collectors.toMap(qd -> qd.id, Function.identity())); - if (collect != null) { - quizMapping.clear(); - quizMapping.putAll(collect); - } - } - - return quizMapping.values(); - - }); - } - - @Override - protected Supplier quizSupplier(final String id) { - return () -> { final Set ids = Stream.of(id).collect(Collectors.toSet()); return getRestTemplate() .map(template -> getQuizzesForIds(template, ids)) .getOr(Collections.emptyList()) .get(0); - }; + }); } @Override - protected Supplier> quizzesSupplier(final Set ids) { - return () -> getRestTemplate() - .map(template -> getQuizzesForIds(template, ids)) - .getOr(Collections.emptyList()); + public void clearCourseCache() { + // TODO Auto-generated method stub } @Override - protected Supplier> allQuizzesSupplier(final FilterMap filterMap) { - return () -> getRestTemplate() - .map(template -> collectAllQuizzes(template, filterMap)) - .getOr(Collections.emptyList()); + public Result getExamineeAccountDetails(final String examineeSessionId) { + return Result.tryCatch(() -> { + + final MoodleAPIRestTemplate template = getRestTemplate() + .getOrThrow(); + + final MultiValueMap queryAttributes = new LinkedMultiValueMap<>(); + queryAttributes.add("field", "id"); + queryAttributes.add("values[0]", examineeSessionId); + + final String userDetailsJSON = template.callMoodleAPIFunction( + MOODLE_USER_PROFILE_API_FUNCTION_NAME, + queryAttributes); + + if (checkAccessDeniedError(userDetailsJSON)) { + final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup(); + log.error("Get access denied error from Moodle: {} for API call: {}, response: {}", + lmsSetup, + MOODLE_USER_PROFILE_API_FUNCTION_NAME, + Utils.truncateText(userDetailsJSON, 2000)); + throw new RuntimeException("No user details on Moodle API request (access-denied)"); + } + + final MoodleUserDetails[] userDetails = this.jsonMapper. readValue( + userDetailsJSON, + new TypeReference() { + }); + + if (userDetails == null || userDetails.length <= 0) { + throw new RuntimeException("No user details on Moodle API request"); + } + + final Map additionalAttributes = new HashMap<>(); + additionalAttributes.put("firstname", userDetails[0].firstname); + additionalAttributes.put("lastname", userDetails[0].lastname); + additionalAttributes.put("department", userDetails[0].department); + additionalAttributes.put("firstaccess", String.valueOf(userDetails[0].firstaccess)); + additionalAttributes.put("lastaccess", String.valueOf(userDetails[0].lastaccess)); + additionalAttributes.put("auth", userDetails[0].auth); + additionalAttributes.put("suspended", String.valueOf(userDetails[0].suspended)); + additionalAttributes.put("confirmed", String.valueOf(userDetails[0].confirmed)); + additionalAttributes.put("lang", userDetails[0].lang); + additionalAttributes.put("theme", userDetails[0].theme); + additionalAttributes.put("timezone", userDetails[0].timezone); + additionalAttributes.put("description", userDetails[0].description); + additionalAttributes.put("mailformat", String.valueOf(userDetails[0].mailformat)); + additionalAttributes.put("descriptionformat", String.valueOf(userDetails[0].descriptionformat)); + return new ExamineeAccountDetails( + userDetails[0].id, + userDetails[0].fullname, + userDetails[0].username, + userDetails[0].email, + additionalAttributes); + }); } @Override - protected Supplier getCourseChaptersSupplier(final String courseId) { - 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 - protected FetchStatus getFetchStatus() { - if (this.allQuizzesRequest.getState() != State.CLOSED) { - return FetchStatus.FETCH_ERROR; - } + public Result getCourseChapters(final String courseId) { + return Result.ofError(new UnsupportedOperationException("not available yet")); + } + + @Override + public FetchStatus getFetchStatus() { + if (this.moodleCourseDataAsyncLoader.isRunning()) { return FetchStatus.ASYNC_FETCH_RUNNING; } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleCourseRestriction.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleCourseRestriction.java index c36ef09e..44716a0f 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleCourseRestriction.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleCourseRestriction.java @@ -22,10 +22,13 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.type.TypeReference; 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.MoodleSEBRestriction; +import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction; 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; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory.MoodleAPIRestTemplate; @@ -57,7 +60,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestT * Delete all key (and remove restrictions): * POST: * http://yourmoodle.org/webservice/rest/server.php?wstoken={token}&moodlewsrestformat=json&wsfunction=seb_restriction_delete&courseId=123 */ -public class MoodleCourseRestriction { +public class MoodleCourseRestriction implements SEBRestrictionAPI { private static final Logger log = LoggerFactory.getLogger(MoodleCourseRestriction.class); @@ -84,7 +87,8 @@ public class MoodleCourseRestriction { this.moodleRestTemplateFactory = moodleRestTemplateFactory; } - LmsSetupTestResult initAPIAccess() { + @Override + public LmsSetupTestResult testCourseRestrictionAPI() { // try to call the SEB Restrictions API try { @@ -108,18 +112,35 @@ public class MoodleCourseRestriction { return LmsSetupTestResult.ofOkay(LmsType.MOODLE); } - Result getSEBRestriction( - final String internalId) { - + @Override + public Result getSEBClientRestriction(final Exam exam) { return Result.tryCatch(() -> { return getSEBRestriction( - MoodleCourseAccess.getQuizId(internalId), - MoodleCourseAccess.getShortname(internalId), - MoodleCourseAccess.getIdnumber(internalId)) + MoodleCourseAccess.getQuizId(exam.externalId), + MoodleCourseAccess.getShortname(exam.externalId), + MoodleCourseAccess.getIdnumber(exam.externalId)) + .map(restriction -> SEBRestriction.from(exam.id, restriction)) .getOrThrow(); }); } + @Override + public Result applySEBClientRestriction( + final String externalExamId, + final SEBRestriction sebRestrictionData) { + + return this.updateSEBRestriction( + externalExamId, + MoodleSEBRestriction.from(sebRestrictionData)) + .map(result -> sebRestrictionData); + } + + @Override + public Result releaseSEBClientRestriction(final Exam exam) { + return this.deleteSEBRestriction(exam.externalId) + .map(result -> exam); + } + Result getSEBRestriction( final String quizId, final String shortname, diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleLmsAPITemplate.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleLmsAPITemplate.java deleted file mode 100644 index 264d24ec..00000000 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleLmsAPITemplate.java +++ /dev/null @@ -1,165 +0,0 @@ -/* - * Copyright (c) 2020 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.moodle; - -import java.util.Collection; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; - -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.Exam; -import ch.ethz.seb.sebserver.gbl.model.exam.MoodleSEBRestriction; -import ch.ethz.seb.sebserver.gbl.model.exam.QuizData; -import ch.ethz.seb.sebserver.gbl.model.exam.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.LmsAPIService; -import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate; -import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.NoSEBRestrictionException; - -/** The MoodleLmsAPITemplate is separated into two parts: - * - MoodleCourseAccess implements the course access API - * - MoodleCourseRestriction implements the SEB restriction API - * - Both uses the MoodleRestTemplateFactore to create a spring based RestTemplate to access the LMS API - * - * NOTE: Because of the missing integration on Moodle side so far the MoodleCourseAccess - * needs to deal with Moodle's standard API functions that don't allow to filter and page course/quiz data - * in an easy and proper way. Therefore we have to fetch all course and quiz data from Moodle before - * filtering and paging can be applied. Since there are possibly thousands of active courses and quizzes - * this moodle course access implements an synchronous fetch as well as an asynchronous fetch strategy. - * The asynchronous fetch strategy is started within a background task that batches the course and quiz - * requests to Moodle and fill up a shared cache. A SEB Server LMS API request will start the - * background task if needed and return immediately to do not block the request. - * The planed Moodle integration on moodle side also defines an improved course access API. This will - * possibly make this synchronous fetch strategy obsolete in the future. */ -public class MoodleLmsAPITemplate implements LmsAPITemplate { - - private static final Logger log = LoggerFactory.getLogger(MoodleLmsAPITemplate.class); - - private final MoodleCourseAccess moodleCourseAccess; - private final MoodleCourseRestriction moodleCourseRestriction; - - protected MoodleLmsAPITemplate( - final MoodleCourseAccess moodleCourseAccess, - final MoodleCourseRestriction moodleCourseRestriction) { - - this.moodleCourseAccess = moodleCourseAccess; - this.moodleCourseRestriction = moodleCourseRestriction; - } - - @Override - public LmsType getType() { - return LmsType.MOODLE; - } - - @Override - public LmsSetup lmsSetup() { - return this.moodleCourseAccess - .getApiTemplateDataSupplier() - .getLmsSetup(); - } - - @Override - public LmsSetupTestResult testCourseAccessAPI() { - return this.moodleCourseAccess.initAPIAccess(); - } - - @Override - public LmsSetupTestResult testCourseRestrictionAPI() { - throw new NoSEBRestrictionException(); - } - - @Override - public Result> getQuizzes(final FilterMap filterMap) { - return this.moodleCourseAccess - .protectedQuizzesRequest(filterMap) - .map(quizzes -> quizzes.stream() - .filter(LmsAPIService.quizFilterPredicate(filterMap)) - .collect(Collectors.toList())); - } - - @Override - public Result getQuiz(final String id) { - return this.moodleCourseAccess.getQuizFromCache(id); - } - - @Override - public Result> getQuizzes(final Set ids) { - return this.moodleCourseAccess.getQuizzesFromCache(ids); - } - - @Override - public void clearCache() { - this.moodleCourseAccess.clearCache(); - } - - @Override - public Result getCourseChapters(final String courseId) { - return Result.tryCatch(() -> this.moodleCourseAccess - .getCourseChaptersSupplier(courseId) - .get()); - } - - @Override - public Result getExamineeAccountDetails(final String examineeSessionId) { - return this.moodleCourseAccess.getExamineeAccountDetails(examineeSessionId); - } - - @Override - public String getExamineeName(final String examineeSessionId) { - return this.moodleCourseAccess.getExamineeName(examineeSessionId); - } - - @Override - public Result getSEBClientRestriction(final Exam exam) { - if (log.isDebugEnabled()) { - log.debug("Get SEB Client restriction for Exam: {}", exam.externalId); - } - - return this.moodleCourseRestriction - .getSEBRestriction(exam.externalId) - .map(restriction -> SEBRestriction.from(exam.id, restriction)); - } - - @Override - public Result applySEBClientRestriction( - final String externalExamId, - final SEBRestriction sebRestrictionData) { - - if (log.isDebugEnabled()) { - log.debug("Apply SEB Client restriction: {}", sebRestrictionData); - } - - return this.moodleCourseRestriction - .updateSEBRestriction( - externalExamId, - MoodleSEBRestriction.from(sebRestrictionData)) - .map(result -> sebRestrictionData); - } - - @Override - public Result releaseSEBClientRestriction(final Exam exam) { - if (log.isDebugEnabled()) { - log.debug("Release SEB Client restriction for Exam: {}", exam); - } - - return this.moodleCourseRestriction - .deleteSEBRestriction(exam.externalId) - .map(result -> exam); - } - -} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleLmsAPITemplateFactory.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleLmsAPITemplateFactory.java index 7659d3a7..3df814d7 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleLmsAPITemplateFactory.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleLmsAPITemplateFactory.java @@ -27,6 +27,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 @@ -88,14 +89,16 @@ public class MoodleLmsAPITemplateFactory implements LmsAPITemplateFactory { this.jsonMapper, moodleRestTemplateFactory, asyncLoaderPrototype, - this.asyncService, this.environment); final MoodleCourseRestriction moodleCourseRestriction = new MoodleCourseRestriction( this.jsonMapper, moodleRestTemplateFactory); - return new MoodleLmsAPITemplate( + return new LmsAPITemplateAdapter( + this.asyncService, + this.environment, + apiTemplateDataSupplier, moodleCourseAccess, moodleCourseRestriction); }); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsAPITemplate.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsAPITemplate.java index 3a84b410..6b27ca98 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsAPITemplate.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsAPITemplate.java @@ -14,8 +14,8 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; -import java.util.function.Supplier; import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; @@ -24,7 +24,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; @@ -35,7 +34,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; @@ -75,11 +73,9 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm 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; @@ -166,7 +162,7 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm @Override public Result> getQuizzes(final FilterMap filterMap) { return this - .protectedQuizzesRequest(filterMap) + .allQuizzesRequest(filterMap) .map(quizzes -> quizzes.stream() .filter(LmsAPIService.quizFilterPredicate(filterMap)) .collect(Collectors.toList())); @@ -187,7 +183,7 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm }); if (!leftIds.isEmpty()) { - result.addAll(super.protectedQuizzesRequest(leftIds).getOrThrow()); + result.addAll(quizzesRequest(leftIds).getOrThrow()); } return result; @@ -201,18 +197,35 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm return Result.of(fromCache); } - return super.protectedQuizRequest(id); + return quizRequest(id); } @Override - protected Supplier> allQuizzesSupplier(final FilterMap filterMap) { - return () -> { + public Result getExamineeAccountDetails(final String examineeUserId) { + return getRestTemplate().map(t -> this.getExamineeById(t, examineeUserId)); + } + + @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.ofError(new UnsupportedOperationException("No Course Chapter available for OpenOLAT LMS")); + } + + protected Result> allQuizzesRequest(final FilterMap filterMap) { + return Result.tryCatch(() -> { final List res = getRestTemplate() .map(t -> this.collectAllQuizzes(t, filterMap)) .getOrThrow(); super.putToCache(res); return res; - }; + }); } private String examUrl(final long olatRepositoryId) { @@ -254,16 +267,15 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm .collect(Collectors.toList()); } - @Override - protected Supplier> quizzesSupplier(final Set ids) { - return () -> ids.stream().map(id -> quizSupplier(id).get()).collect(Collectors.toList()); + protected Result> quizzesRequest(final Set ids) { + return Result.tryCatch(() -> ids.stream() + .map(id -> quizRequest(id).getOr(null)) + .filter(Objects::nonNull) + .collect(Collectors.toList())); } - @Override - protected Supplier quizSupplier(final String id) { - return () -> getRestTemplate() - .map(t -> this.quizById(t, id)) - .getOrThrow(); + protected Result quizRequest(final String id) { + return getRestTemplate().map(t -> this.quizById(t, id)); } private QuizData quizById(final OlatLmsRestTemplate restTemplate, final String id) { @@ -295,20 +307,6 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm attrs); } - @Override - protected Supplier accountDetailsSupplier(final String id) { - return () -> getRestTemplate() - .map(t -> this.getExamineeById(t, id)) - .getOrThrow(); - } - - @Override - protected Supplier getCourseChaptersSupplier(final String courseId) { - return () -> { - throw new UnsupportedOperationException("No Course Chapter available for OpenOLAT LMS"); - }; - } - private SEBRestriction getRestrictionForAssignmentId(final RestTemplate restTemplate, final String id) { final String url = String.format("/restapi/assessment_modes/%s/seb_restriction", id); final RestrictionData r = this.apiGet(restTemplate, url, RestrictionData.class); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsAPITemplateFactory.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsAPITemplateFactory.java index cb4b71a9..3330737b 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsAPITemplateFactory.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsAPITemplateFactory.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 OlatLmsAPITemplateFactory implements LmsAPITemplateFactory { @Override public Result create(final APITemplateDataSupplier apiTemplateDataSupplier) { return Result.tryCatch(() -> { - return new OlatLmsAPITemplate( + final OlatLmsAPITemplate olatLmsAPITemplate = new OlatLmsAPITemplate( this.clientHttpRequestFactoryService, this.clientCredentialService, apiTemplateDataSupplier, + this.cacheManager); + return new LmsAPITemplateAdapter( this.asyncService, this.environment, - this.cacheManager); + apiTemplateDataSupplier, + olatLmsAPITemplate, + olatLmsAPITemplate); }); } diff --git a/src/main/resources/config/application-dev.properties b/src/main/resources/config/application-dev.properties index e3848c27..88b4f69f 100644 --- a/src/main/resources/config/application-dev.properties +++ b/src/main/resources/config/application-dev.properties @@ -11,7 +11,7 @@ logging.level.ROOT=INFO logging.level.ch=INFO logging.level.ch.ethz.seb.sebserver.webservice.datalayer=INFO logging.level.org.springframework.cache=INFO -logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl=INFO +logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl=DEBUG logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.session=DEBUG logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.proctoring=INFO logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.indicator=DEBUG diff --git a/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleCourseAccessTest.java b/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleCourseAccessTest.java index 8cd82b35..74a1b617 100644 --- a/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleCourseAccessTest.java +++ b/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleCourseAccessTest.java @@ -22,8 +22,6 @@ import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import ch.ethz.seb.sebserver.gbl.api.JSONMapper; -import ch.ethz.seb.sebserver.gbl.async.AsyncRunner; -import ch.ethz.seb.sebserver.gbl.async.AsyncService; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult.ErrorType; @@ -75,7 +73,6 @@ public class MoodleCourseAccessTest { new JSONMapper(), moodleRestTemplateFactory, null, - new AsyncService(new AsyncRunner()), this.env); final String examId = "123"; @@ -123,10 +120,9 @@ public class MoodleCourseAccessTest { new JSONMapper(), moodleRestTemplateFactory, null, - mock(AsyncService.class), this.env); - final LmsSetupTestResult initAPIAccess = moodleCourseAccess.initAPIAccess(); + final LmsSetupTestResult initAPIAccess = moodleCourseAccess.testCourseAccessAPI(); assertNotNull(initAPIAccess); assertFalse(initAPIAccess.errors.isEmpty()); assertTrue(initAPIAccess.hasError(ErrorType.TOKEN_REQUEST)); @@ -145,10 +141,9 @@ public class MoodleCourseAccessTest { new JSONMapper(), moodleRestTemplateFactory, null, - mock(AsyncService.class), this.env); - final LmsSetupTestResult initAPIAccess = moodleCourseAccess.initAPIAccess(); + final LmsSetupTestResult initAPIAccess = moodleCourseAccess.testCourseAccessAPI(); assertNotNull(initAPIAccess); assertFalse(initAPIAccess.errors.isEmpty()); assertTrue(initAPIAccess.hasError(ErrorType.QUIZ_ACCESS_API_REQUEST)); @@ -166,10 +161,9 @@ public class MoodleCourseAccessTest { new JSONMapper(), moodleRestTemplateFactory, null, - mock(AsyncService.class), this.env); - final LmsSetupTestResult initAPIAccess = moodleCourseAccess.initAPIAccess(); + final LmsSetupTestResult initAPIAccess = moodleCourseAccess.testCourseAccessAPI(); assertNotNull(initAPIAccess); assertTrue(initAPIAccess.errors.isEmpty());