SEBSERV-158 preparation - refactoring of LmsAPITemplate

This commit is contained in:
anhefti 2022-03-21 13:37:33 +01:00
parent 8dae41f754
commit eb08df6c00
26 changed files with 1204 additions and 1187 deletions

View file

@ -56,7 +56,7 @@ public final class LmsSetup implements GrantEntity, Activatable {
public enum LmsType { public enum LmsType {
/** Mockup LMS type used to create test setups */ /** Mockup LMS type used to create test setups */
MOCKUP(Features.COURSE_API), MOCKUP(Features.COURSE_API),
/** The Open edX LMS binding features both APIs, course access as well as SEB restrcition */ /** The Open edX LMS binding features both APIs, course access as well as SEB restriction */
OPEN_EDX(Features.COURSE_API, Features.SEB_RESTRICTION), OPEN_EDX(Features.COURSE_API, Features.SEB_RESTRICTION),
/** The Moodle binding features only the course access API so far */ /** The Moodle binding features only the course access API so far */
MOODLE(Features.COURSE_API /* , Features.SEB_RESTRICTION */), MOODLE(Features.COURSE_API /* , Features.SEB_RESTRICTION */),

View file

@ -28,6 +28,7 @@ public final class LmsSetupTestResult {
public static final String ATTR_MISSING_ATTRIBUTE = "missingLMSSetupAttribute"; public static final String ATTR_MISSING_ATTRIBUTE = "missingLMSSetupAttribute";
public enum ErrorType { public enum ErrorType {
API_NOT_SUPPORTED,
MISSING_ATTRIBUTE, MISSING_ATTRIBUTE,
TOKEN_REQUEST, TOKEN_REQUEST,
QUIZ_ACCESS_API_REQUEST, QUIZ_ACCESS_API_REQUEST,
@ -109,7 +110,12 @@ public final class LmsSetupTestResult {
return new LmsSetupTestResult(lmsType); 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<APIMessage> attrs) { final Collection<APIMessage> attrs) {
return new LmsSetupTestResult(lmsType, new Error(ErrorType.MISSING_ATTRIBUTE, "missing attribute(s)"), attrs); return new LmsSetupTestResult(lmsType, new Error(ErrorType.MISSING_ATTRIBUTE, "missing attribute(s)"), attrs);
} }

View file

@ -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:
*
* <pre>
* {@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)
* </pre>
*
* @return Result of an unsorted List of filtered {@link QuizData } from the LMS course/quiz API
* or refer to an error when happened */
Result<List<QuizData>> 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<Collection<QuizData>> getQuizzes(Set<String> 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<QuizData> 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<ExamineeAccountDetails> 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<Chapters> getCourseChapters(String courseId);
default FetchStatus getFetchStatus() {
return FetchStatus.ALL_FETCHED;
}
}

View file

@ -8,20 +8,9 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.lms; 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.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.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails;
import ch.ethz.seb.sebserver.gbl.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.AbstractCachedCourseAccess;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.AbstractCourseAccess; 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 * or partial API Access and can flag missing or wrong {@link LmsSetup } attributes with the resulting
* {@link LmsSetupTestResult }.</br> * {@link LmsSetupTestResult }.</br>
* SEB Server than uses an instance of this template to communicate with the an LMS. */ * SEB Server than uses an instance of this template to communicate with the an LMS. */
public interface LmsAPITemplate { public interface LmsAPITemplate extends CourseAccessAPI, SEBRestrictionAPI {
/** Get the LMS type of the concrete template implementation /** 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 */ * @return the underling {@link LmsSetup } configuration for this LmsAPITemplate */
LmsSetup lmsSetup(); LmsSetup lmsSetup();
// ******************************************************************* default void dispose() {
// **** Course API functions ***************************************** clearCourseCache();
}
/** 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:
*
* <pre>
* {@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)
* </pre>
*
* @return Result of an unsorted List of filtered {@link QuizData } from the LMS course/quiz API
* or refer to an error when happened */
Result<List<QuizData>> 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<Collection<QuizData>> getQuizzes(Set<String> 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<QuizData> 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<ExamineeAccountDetails> 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<Chapters> 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<SEBRestriction> 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<SEBRestriction> 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<Exam> releaseSEBClientRestriction(Exam exam);
} }

View file

@ -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<SEBRestriction> 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<SEBRestriction> 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<Exam> releaseSEBClientRestriction(Exam exam);
}

View file

@ -16,10 +16,8 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.cache.Cache; import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager; import org.springframework.cache.CacheManager;
import org.springframework.core.env.Environment;
import ch.ethz.seb.sebserver.gbl.Constants; import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.async.AsyncService;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData; import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
/** This implements an overall short time cache for QuizData objects for all implementing /** 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 QuizData are stored with a key composed from the id of the key
* </p> * </p>
* The EH-Cache can be configured in file ehcache.xml **/ * 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); private static final Logger log = LoggerFactory.getLogger(AbstractCachedCourseAccess.class);
@ -37,17 +35,12 @@ public abstract class AbstractCachedCourseAccess extends AbstractCourseAccess {
private final Cache cache; private final Cache cache;
protected AbstractCachedCourseAccess( protected AbstractCachedCourseAccess(final CacheManager cacheManager) {
final AsyncService asyncService,
final Environment environment,
final CacheManager cacheManager) {
super(asyncService, environment);
this.cache = cacheManager.getCache(CACHE_NAME_QUIZ_DATA); this.cache = cacheManager.getCache(CACHE_NAME_QUIZ_DATA);
} }
/** Used to clear the entire cache */ /** Used to clear the entire cache */
public void clearCache() { public void clearCourseCache() {
final Object nativeCache = this.cache.getNativeCache(); final Object nativeCache = this.cache.getNativeCache();
if (nativeCache instanceof javax.cache.Cache) { if (nativeCache instanceof javax.cache.Cache) {
try { try {

View file

@ -20,7 +20,6 @@ import org.springframework.core.env.Environment;
import ch.ethz.seb.sebserver.gbl.Constants; import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.async.AsyncService; import ch.ethz.seb.sebserver.gbl.async.AsyncService;
import ch.ethz.seb.sebserver.gbl.async.CircuitBreaker; 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.Chapters;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData; import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails; 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); 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 */ /** CircuitBreaker for protected quiz and course data requests */
protected final CircuitBreaker<List<QuizData>> allQuizzesRequest; protected final CircuitBreaker<List<QuizData>> allQuizzesRequest;
/** CircuitBreaker for protected quiz and course data requests */ /** 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 */ /** Provides a supplier for the course chapter data request to use within the circuit breaker */
protected abstract Supplier<Chapters> getCourseChaptersSupplier(final String courseId); protected abstract Supplier<Chapters> getCourseChaptersSupplier(final String courseId);
protected FetchStatus getFetchStatus() {
if (this.quizzesRequest.getState() != State.CLOSED) {
return FetchStatus.FETCH_ERROR;
}
return FetchStatus.ALL_FETCHED;
}
} }

View file

@ -96,12 +96,13 @@ public class LmsAPIServiceImpl implements LmsAPIService {
final LmsAPITemplate removedTemplate = this.cache final LmsAPITemplate removedTemplate = this.cache
.remove(new CacheKey(lmsSetup.getModelId(), 0)); .remove(new CacheKey(lmsSetup.getModelId(), 0));
if (removedTemplate != null) { if (removedTemplate != null) {
removedTemplate.clearCache(); removedTemplate.clearCourseCache();
} }
} }
@Override @Override
public void cleanup() { public void cleanup() {
this.cache.values().forEach(LmsAPITemplate::dispose);
this.cache.clear(); this.cache.clear();
} }

View file

@ -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<List<QuizData>> allQuizzesRequest;
/** CircuitBreaker for protected quiz and course data requests */
private final CircuitBreaker<Collection<QuizData>> quizzesRequest;
/** CircuitBreaker for protected quiz and course data requests */
private final CircuitBreaker<QuizData> quizRequest;
/** CircuitBreaker for protected chapter data requests */
private final CircuitBreaker<Chapters> chaptersRequest;
/** CircuitBreaker for protected examinee account details requests */
private final CircuitBreaker<ExamineeAccountDetails> accountDetailRequest;
private final CircuitBreaker<SEBRestriction> restrictionRequest;
private final CircuitBreaker<Exam> 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<List<QuizData>> 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<Collection<QuizData>> getQuizzes(final Set<String> 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<QuizData> 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<ExamineeAccountDetails> 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<Chapters> 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<SEBRestriction> 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<SEBRestriction> 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<Exam> 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());
}
}

View file

@ -18,7 +18,6 @@ import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -28,7 +27,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.cache.CacheManager; import org.springframework.cache.CacheManager;
import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpEntity; import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
@ -39,7 +37,6 @@ import org.springframework.web.client.RestTemplate;
import ch.ethz.seb.sebserver.ClientHttpRequestFactoryService; import ch.ethz.seb.sebserver.ClientHttpRequestFactoryService;
import ch.ethz.seb.sebserver.gbl.api.APIMessage; 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.ClientCredentialService;
import ch.ethz.seb.sebserver.gbl.client.ClientCredentials; import ch.ethz.seb.sebserver.gbl.client.ClientCredentials;
import ch.ethz.seb.sebserver.gbl.client.ProxyData; import ch.ethz.seb.sebserver.gbl.client.ProxyData;
@ -78,11 +75,9 @@ public class AnsLmsAPITemplate extends AbstractCachedCourseAccess implements Lms
final ClientHttpRequestFactoryService clientHttpRequestFactoryService, final ClientHttpRequestFactoryService clientHttpRequestFactoryService,
final ClientCredentialService clientCredentialService, final ClientCredentialService clientCredentialService,
final APITemplateDataSupplier apiTemplateDataSupplier, final APITemplateDataSupplier apiTemplateDataSupplier,
final AsyncService asyncService,
final Environment environment,
final CacheManager cacheManager) { final CacheManager cacheManager) {
super(asyncService, environment, cacheManager); super(cacheManager);
this.clientHttpRequestFactoryService = clientHttpRequestFactoryService; this.clientHttpRequestFactoryService = clientHttpRequestFactoryService;
this.clientCredentialService = clientCredentialService; this.clientCredentialService = clientCredentialService;
@ -170,7 +165,7 @@ public class AnsLmsAPITemplate extends AbstractCachedCourseAccess implements Lms
@Override @Override
public Result<List<QuizData>> getQuizzes(final FilterMap filterMap) { public Result<List<QuizData>> getQuizzes(final FilterMap filterMap) {
return this return this
.protectedQuizzesRequest(filterMap) .allQuizzesRequest(filterMap)
.map(quizzes -> quizzes.stream() .map(quizzes -> quizzes.stream()
.filter(LmsAPIService.quizFilterPredicate(filterMap)) .filter(LmsAPIService.quizFilterPredicate(filterMap))
.collect(Collectors.toList())); .collect(Collectors.toList()));
@ -191,7 +186,7 @@ public class AnsLmsAPITemplate extends AbstractCachedCourseAccess implements Lms
}); });
if (!leftIds.isEmpty()) { if (!leftIds.isEmpty()) {
result.addAll(super.protectedQuizzesRequest(leftIds).getOrThrow()); result.addAll(quizzesRequest(leftIds).getOrThrow());
} }
return result; return result;
@ -205,7 +200,7 @@ public class AnsLmsAPITemplate extends AbstractCachedCourseAccess implements Lms
return Result.of(fromCache); return Result.of(fromCache);
} }
return super.protectedQuizRequest(id); return quizRequest(id);
} }
private List<QuizData> collectAllQuizzes(final AnsPersonalRestTemplate restTemplate) { private List<QuizData> collectAllQuizzes(final AnsPersonalRestTemplate restTemplate) {
@ -279,33 +274,28 @@ public class AnsLmsAPITemplate extends AbstractCachedCourseAccess implements Lms
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
@Override protected Result<List<QuizData>> allQuizzesRequest(final FilterMap filterMap) {
protected Supplier<List<QuizData>> allQuizzesSupplier(final FilterMap filterMap) {
// We cannot filter by from-date or partial names using the Ans search API. // 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 // Only exact matches are permitted. So we're not implementing filtering
// on the API level and always retrieve all assignments and let SEB server // on the API level and always retrieve all assignments and let SEB server
// do the filtering. // do the filtering.
return () -> { return Result.tryCatch(() -> {
final List<QuizData> res = getRestTemplate() final List<QuizData> res = getRestTemplate()
.map(this::collectAllQuizzes) .map(this::collectAllQuizzes)
.getOrThrow(); .getOrThrow();
super.putToCache(res); super.putToCache(res);
return res; return res;
}; });
} }
@Override protected Result<Collection<QuizData>> quizzesRequest(final Set<String> ids) {
protected Supplier<Collection<QuizData>> quizzesSupplier(final Set<String> ids) { return getRestTemplate()
return () -> getRestTemplate() .map(t -> this.getQuizzesByIds(t, ids));
.map(t -> this.getQuizzesByIds(t, ids))
.getOrThrow();
} }
@Override protected Result<QuizData> quizRequest(final String id) {
protected Supplier<QuizData> quizSupplier(final String id) { return getRestTemplate()
return () -> getRestTemplate() .map(t -> this.getQuizByAssignmentId(t, id));
.map(t -> this.getQuizByAssignmentId(t, id))
.getOrThrow();
} }
private ExamineeAccountDetails getExamineeById(final RestTemplate restTemplate, final String id) { private ExamineeAccountDetails getExamineeById(final RestTemplate restTemplate, final String id) {
@ -324,17 +314,21 @@ public class AnsLmsAPITemplate extends AbstractCachedCourseAccess implements Lms
} }
@Override @Override
protected Supplier<ExamineeAccountDetails> accountDetailsSupplier(final String id) { public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeUserId) {
return () -> getRestTemplate() return getRestTemplate().map(t -> this.getExamineeById(t, examineeUserId));
.map(t -> this.getExamineeById(t, id))
.getOrThrow();
} }
@Override @Override
protected Supplier<Chapters> getCourseChaptersSupplier(final String courseId) { public String getExamineeName(final String examineeUserId) {
return () -> { return getExamineeAccountDetails(examineeUserId)
throw new UnsupportedOperationException("not available yet"); .map(ExamineeAccountDetails::getDisplayName)
}; .onError(error -> log.warn("Failed to request user-name for ID: {}", error.getMessage(), error))
.getOr(examineeUserId);
}
@Override
public Result<Chapters> getCourseChapters(final String courseId) {
return Result.ofError(new UnsupportedOperationException("not available yet"));
} }
@Override @Override

View file

@ -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.APITemplateDataSupplier;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplateFactory; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplateFactory;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.LmsAPITemplateAdapter;
@Lazy @Lazy
@Service @Service
@ -63,13 +64,17 @@ public class AnsLmsAPITemplateFactory implements LmsAPITemplateFactory {
@Override @Override
public Result<LmsAPITemplate> create(final APITemplateDataSupplier apiTemplateDataSupplier) { public Result<LmsAPITemplate> create(final APITemplateDataSupplier apiTemplateDataSupplier) {
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
return new AnsLmsAPITemplate( final AnsLmsAPITemplate ansLmsAPITemplate = new AnsLmsAPITemplate(
this.clientHttpRequestFactoryService, this.clientHttpRequestFactoryService,
this.clientCredentialService, this.clientCredentialService,
apiTemplateDataSupplier, apiTemplateDataSupplier,
this.cacheManager);
return new LmsAPITemplateAdapter(
this.asyncService, this.asyncService,
this.environment, this.environment,
this.cacheManager); apiTemplateDataSupplier,
ansLmsAPITemplate,
ansLmsAPITemplate);
}); });
} }

View file

@ -17,14 +17,12 @@ import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.cache.CacheManager; import org.springframework.cache.CacheManager;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpEntity; import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
@ -45,7 +43,6 @@ import com.fasterxml.jackson.core.type.TypeReference;
import ch.ethz.seb.sebserver.gbl.Constants; import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.JSONMapper; import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
import ch.ethz.seb.sebserver.gbl.async.AsyncService;
import ch.ethz.seb.sebserver.gbl.model.exam.Chapters; 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.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; 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.WebserviceInfo;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.CourseAccessAPI;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.AbstractCachedCourseAccess; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.AbstractCachedCourseAccess;
/** Implements the LmsAPITemplate for Open edX LMS Course API access. /** Implements the LmsAPITemplate for Open edX LMS Course API access.
* *
* See also: https://course-catalog-api-guide.readthedocs.io */ * See also: https://course-catalog-api-guide.readthedocs.io */
final class OpenEdxCourseAccess extends AbstractCachedCourseAccess { final class OpenEdxCourseAccess extends AbstractCachedCourseAccess implements CourseAccessAPI {
private static final Logger log = LoggerFactory.getLogger(OpenEdxCourseAccess.class); private static final Logger log = LoggerFactory.getLogger(OpenEdxCourseAccess.class);
@ -84,11 +82,9 @@ final class OpenEdxCourseAccess extends AbstractCachedCourseAccess {
final JSONMapper jsonMapper, final JSONMapper jsonMapper,
final OpenEdxRestTemplateFactory openEdxRestTemplateFactory, final OpenEdxRestTemplateFactory openEdxRestTemplateFactory,
final WebserviceInfo webserviceInfo, final WebserviceInfo webserviceInfo,
final AsyncService asyncService,
final Environment environment,
final CacheManager cacheManager) { final CacheManager cacheManager) {
super(asyncService, environment, cacheManager); super(cacheManager);
this.jsonMapper = jsonMapper; this.jsonMapper = jsonMapper;
this.openEdxRestTemplateFactory = openEdxRestTemplateFactory; this.openEdxRestTemplateFactory = openEdxRestTemplateFactory;
this.webserviceInfo = webserviceInfo; this.webserviceInfo = webserviceInfo;
@ -104,7 +100,8 @@ final class OpenEdxCourseAccess extends AbstractCachedCourseAccess {
return this.lmsSetupId; return this.lmsSetupId;
} }
LmsSetupTestResult initAPIAccess() { @Override
public LmsSetupTestResult testCourseAccessAPI() {
final LmsSetupTestResult attributesCheck = this.openEdxRestTemplateFactory.test(); final LmsSetupTestResult attributesCheck = this.openEdxRestTemplateFactory.test();
if (!attributesCheck.isOk()) { if (!attributesCheck.isOk()) {
@ -141,68 +138,18 @@ final class OpenEdxCourseAccess extends AbstractCachedCourseAccess {
} }
@Override @Override
protected Supplier<ExamineeAccountDetails> accountDetailsSupplier(final String examineeSessionId) { public Result<List<QuizData>> getQuizzes(final FilterMap filterMap) {
return () -> { return getRestTemplate().map(this::collectAllQuizzes);
try { }
final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup();
final HttpHeaders httpHeaders = new HttpHeaders();
final OAuth2RestTemplate template = getRestTemplate()
.getOrThrow();
final String externalStartURI = this.webserviceInfo @Override
.getLmsExternalAddressAlias(lmsSetup.lmsApiUrl); public Result<QuizData> getQuiz(final String id) {
return Result.tryCatch(() -> {
final String uri = (externalStartURI != null) final QuizData fromCache = super.getFromCache(id);
? externalStartURI + OPEN_EDX_DEFAULT_USER_PROFILE_ENDPOINT + examineeSessionId if (fromCache != null) {
: lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_USER_PROFILE_ENDPOINT + examineeSessionId; return fromCache;
final String responseJSON = template.exchange(
uri,
HttpMethod.GET,
new HttpEntity<>(httpHeaders),
String.class)
.getBody();
final EdxUserDetails[] userDetails = this.jsonMapper.<EdxUserDetails[]> readValue(
responseJSON,
new TypeReference<EdxUserDetails[]>() {
});
if (userDetails == null || userDetails.length <= 0) {
throw new RuntimeException("No user details on Open edX API request");
}
final Map<String, String> 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
protected Supplier<List<QuizData>> allQuizzesSupplier(final FilterMap filterMap) {
return () -> getRestTemplate()
.map(this::collectAllQuizzes)
.getOrThrow();
}
@Override
protected Supplier<QuizData> quizSupplier(final String id) {
return () -> {
final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup(); final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup();
final String externalStartURI = getExternalLMSServerAddress(lmsSetup); final String externalStartURI = getExternalLMSServerAddress(lmsSetup);
final QuizData quizData = quizDataOf( final QuizData quizData = quizDataOf(
@ -214,13 +161,13 @@ final class OpenEdxCourseAccess extends AbstractCachedCourseAccess {
super.putToCache(quizData); super.putToCache(quizData);
} }
return quizData; return quizData;
}; });
} }
@Override @Override
protected Supplier<Collection<QuizData>> quizzesSupplier(final Set<String> ids) { public Result<Collection<QuizData>> getQuizzes(final Set<String> ids) {
if (ids.size() == 1) { if (ids.size() == 1) {
return () -> { return Result.tryCatch(() -> {
final String id = ids.iterator().next(); final String id = ids.iterator().next();
@ -239,17 +186,73 @@ final class OpenEdxCourseAccess extends AbstractCachedCourseAccess {
getRestTemplate().getOrThrow(), getRestTemplate().getOrThrow(),
id), id),
externalStartURI)); externalStartURI));
}; });
} else { } else {
return () -> getRestTemplate() return getRestTemplate().map(template -> this.collectQuizzes(template, ids));
.map(template -> this.collectQuizzes(template, ids))
.getOrThrow();
} }
} }
@Override @Override
protected Supplier<Chapters> getCourseChaptersSupplier(final String courseId) { public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeUserId) {
return () -> { 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.<EdxUserDetails[]> readValue(
responseJSON,
new TypeReference<EdxUserDetails[]>() {
});
if (userDetails == null || userDetails.length <= 0) {
throw new RuntimeException("No user details on Open edX API request");
}
final Map<String, String> 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<Chapters> getCourseChapters(final String courseId) {
return Result.tryCatch(() -> {
final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup(); final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup();
final String uri = final String uri =
@ -262,7 +265,7 @@ final class OpenEdxCourseAccess extends AbstractCachedCourseAccess {
.filter(block -> OPEN_EDX_DEFAULT_BLOCKS_TYPE_CHAPTER.equals(block.type)) .filter(block -> OPEN_EDX_DEFAULT_BLOCKS_TYPE_CHAPTER.equals(block.type))
.map(block -> new Chapters.Chapter(block.display_name, block.block_id)) .map(block -> new Chapters.Chapter(block.display_name, block.block_id))
.collect(Collectors.toList())); .collect(Collectors.toList()));
}; });
} }
public Result<Collection<QuizData>> getQuizzesFromCache(final Set<String> ids) { public Result<Collection<QuizData>> getQuizzesFromCache(final Set<String> ids) {
@ -279,7 +282,7 @@ final class OpenEdxCourseAccess extends AbstractCachedCourseAccess {
}); });
if (!leftIds.isEmpty()) { if (!leftIds.isEmpty()) {
result.addAll(super.protectedQuizzesRequest(leftIds).getOrThrow()); result.addAll(getQuizzes(leftIds).getOrThrow());
} }
return result; return result;

View file

@ -22,17 +22,20 @@ import org.springframework.web.client.HttpClientErrorException;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import ch.ethz.seb.sebserver.gbl.api.JSONMapper; 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.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;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.util.Result; 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.NoSEBRestrictionException;
/** The open edX SEB course restriction API implementation. /** The open edX SEB course restriction API implementation.
* *
* See also : https://seb-openedx.readthedocs.io/en/latest/ */ * 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); private static final Logger log = LoggerFactory.getLogger(OpenEdxCourseRestriction.class);
@ -54,7 +57,8 @@ public class OpenEdxCourseRestriction {
this.openEdxRestTemplateFactory = openEdxRestTemplateFactory; this.openEdxRestTemplateFactory = openEdxRestTemplateFactory;
} }
LmsSetupTestResult initAPIAccess() { @Override
public LmsSetupTestResult testCourseRestrictionAPI() {
final LmsSetupTestResult attributesCheck = this.openEdxRestTemplateFactory.test(); final LmsSetupTestResult attributesCheck = this.openEdxRestTemplateFactory.test();
if (!attributesCheck.isOk()) { if (!attributesCheck.isOk()) {
@ -95,21 +99,22 @@ public class OpenEdxCourseRestriction {
} }
if (log.isDebugEnabled()) { 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); return LmsSetupTestResult.ofOkay(LmsType.OPEN_EDX);
} }
Result<OpenEdxSEBRestriction> getSEBRestriction(final String courseId) { @Override
public Result<SEBRestriction> getSEBClientRestriction(final Exam exam) {
if (log.isDebugEnabled()) { 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(); final LmsSetup lmsSetup = this.openEdxRestTemplateFactory.apiTemplateDataSupplier.getLmsSetup();
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
final String url = lmsSetup.lmsApiUrl + getSEBRestrictionUrl(courseId); final String url = lmsSetup.lmsApiUrl + getSEBRestrictionUrl(courseId);
final HttpHeaders httpHeaders = new HttpHeaders(); final HttpHeaders httpHeaders = new HttpHeaders();
@ -127,7 +132,7 @@ public class OpenEdxCourseRestriction {
if (log.isDebugEnabled()) { if (log.isDebugEnabled()) {
log.debug("Successfully GET SEB Client restriction on course: {}", courseId); log.debug("Successfully GET SEB Client restriction on course: {}", courseId);
} }
return data; return SEBRestriction.from(exam.id, data);
} catch (final HttpClientErrorException ce) { } catch (final HttpClientErrorException ce) {
if (ce.getStatusCode() == HttpStatus.NOT_FOUND || ce.getStatusCode() == HttpStatus.UNAUTHORIZED) { if (ce.getStatusCode() == HttpStatus.NOT_FOUND || ce.getStatusCode() == HttpStatus.UNAUTHORIZED) {
throw new NoSEBRestrictionException(ce); throw new NoSEBRestrictionException(ce);
@ -137,17 +142,18 @@ public class OpenEdxCourseRestriction {
}); });
} }
Result<Boolean> putSEBRestriction( @Override
final String courseId, public Result<SEBRestriction> applySEBClientRestriction(
final OpenEdxSEBRestriction restriction) { final String externalExamId,
final SEBRestriction sebRestrictionData) {
if (log.isDebugEnabled()) { 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(() -> { return Result.tryCatch(() -> {
final LmsSetup lmsSetup = this.openEdxRestTemplateFactory.apiTemplateDataSupplier.getLmsSetup(); 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(); final HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); httpHeaders.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
httpHeaders.add(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate"); httpHeaders.add(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate");
@ -157,24 +163,26 @@ public class OpenEdxCourseRestriction {
.exchange( .exchange(
url, url,
HttpMethod.PUT, HttpMethod.PUT,
new HttpEntity<>(toJson(restriction), httpHeaders), new HttpEntity<>(toJson(OpenEdxSEBRestriction.from(sebRestrictionData)), httpHeaders),
OpenEdxSEBRestriction.class) OpenEdxSEBRestriction.class)
.getBody(); .getBody();
if (log.isDebugEnabled()) { 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<Boolean> deleteSEBRestriction(final String courseId) { @Override
public Result<Exam> releaseSEBClientRestriction(final Exam exam) {
if (log.isDebugEnabled()) { 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(() -> { return Result.tryCatch(() -> {
final LmsSetup lmsSetup = this.openEdxRestTemplateFactory.apiTemplateDataSupplier.getLmsSetup(); final LmsSetup lmsSetup = this.openEdxRestTemplateFactory.apiTemplateDataSupplier.getLmsSetup();
final String url = lmsSetup.lmsApiUrl + getSEBRestrictionUrl(courseId); final String url = lmsSetup.lmsApiUrl + getSEBRestrictionUrl(courseId);
@ -193,7 +201,7 @@ public class OpenEdxCourseRestriction {
if (log.isDebugEnabled()) { if (log.isDebugEnabled()) {
log.debug("Successfully PUT SEB Client restriction on course: {}", courseId); log.debug("Successfully PUT SEB Client restriction on course: {}", courseId);
} }
return true; return exam;
} else { } else {
throw new RuntimeException("Unexpected response for deletion: " + exchange); 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<Object> exchange = this.restTemplate.exchange(
// url,
// HttpMethod.DELETE,
// new HttpEntity<>(httpHeaders),
// Object.class);
//
// if (exchange.getStatusCode() == HttpStatus.NO_CONTENT) {
// if (log.isDebugEnabled()) {
// log.debug("Successfully PUT SEB Client restriction on course: {}", courseId);
// }
// } else {
// log.error("Unexpected response for deletion: {}", exchange);
// return false;
// }
//
// return true;
// };
// }
// private Result<Boolean> handleSEBRestriction(final BooleanSupplier task) {
// return getRestTemplate()
// .map(restTemplate -> {
// try {
// return task.getAsBoolean();
// } catch (final HttpClientErrorException ce) {
// if (ce.getStatusCode() == HttpStatus.UNAUTHORIZED) {
// throw new APIMessageException(APIMessage.ErrorMessage.UNAUTHORIZED.of(ce.getMessage()
// + " Unable to get access for API. Please check the corresponding LMS Setup "));
// }
// throw ce;
// } catch (final Exception e) {
// throw new RuntimeException("Unexpected: ", e);
// }
// });
// }
private String getSEBRestrictionUrl(final String courseId) { private String getSEBRestrictionUrl(final String courseId) {
return String.format(OPEN_EDX_DEFAULT_COURSE_RESTRICTION_API_PATH, courseId); return String.format(OPEN_EDX_DEFAULT_COURSE_RESTRICTION_API_PATH, courseId);
} }

View file

@ -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<List<QuizData>> getQuizzes(final FilterMap filterMap) {
return this.openEdxCourseAccess
.protectedQuizzesRequest(filterMap)
.map(quizzes -> quizzes.stream()
.filter(LmsAPIService.quizFilterPredicate(filterMap))
.collect(Collectors.toList()));
}
@Override
public Result<QuizData> 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<Collection<QuizData>> getQuizzes(final Set<String> ids) {
return this.openEdxCourseAccess.getQuizzesFromCache(ids);
}
@Override
public void clearCache() {
this.openEdxCourseAccess.clearCache();
}
@Override
public Result<Chapters> getCourseChapters(final String courseId) {
return Result.tryCatch(() -> this.openEdxCourseAccess
.getCourseChaptersSupplier(courseId)
.get());
}
@Override
public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeSessionId) {
return this.openEdxCourseAccess.getExamineeAccountDetails(examineeSessionId);
}
@Override
public String getExamineeName(final String examineeSessionId) {
return this.openEdxCourseAccess.getExamineeName(examineeSessionId);
}
@Override
public Result<SEBRestriction> 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<SEBRestriction> 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<Exam> 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);
}
}

View file

@ -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.APITemplateDataSupplier;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplateFactory; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplateFactory;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.LmsAPITemplateAdapter;
@Lazy @Lazy
@Service @Service
@ -87,8 +88,6 @@ public class OpenEdxLmsAPITemplateFactory implements LmsAPITemplateFactory {
this.jsonMapper, this.jsonMapper,
openEdxRestTemplateFactory, openEdxRestTemplateFactory,
this.webserviceInfo, this.webserviceInfo,
this.asyncService,
this.environment,
this.cacheManager); this.cacheManager);
final OpenEdxCourseRestriction openEdxCourseRestriction = new OpenEdxCourseRestriction( final OpenEdxCourseRestriction openEdxCourseRestriction = new OpenEdxCourseRestriction(
@ -96,7 +95,10 @@ public class OpenEdxLmsAPITemplateFactory implements LmsAPITemplateFactory {
openEdxRestTemplateFactory, openEdxRestTemplateFactory,
this.restrictionAPIPushCount); this.restrictionAPIPushCount);
return new OpenEdxLmsAPITemplate( return new LmsAPITemplateAdapter(
this.asyncService,
this.environment,
apiTemplateDataSupplier,
openEdxCourseAccess, openEdxCourseAccess,
openEdxCourseRestriction); openEdxCourseRestriction);
}); });

View file

@ -1,323 +1,224 @@
/* /*
* Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET) * Copyright (c) 2022 ETH Zürich, Educational Development and Technology (LET)
* *
* This Source Code Form is subject to the terms of the Mozilla Public * 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 * 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/. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/ */
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.mockup; package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.mockup;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.function.Supplier; import java.util.stream.Collectors;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.StringUtils; import org.joda.time.DateTime;
import org.joda.time.DateTime; import org.joda.time.DateTimeZone;
import org.joda.time.DateTimeZone;
import org.slf4j.Logger; import ch.ethz.seb.sebserver.gbl.Constants;
import org.slf4j.LoggerFactory; import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import org.springframework.core.env.Environment; import ch.ethz.seb.sebserver.gbl.client.ClientCredentials;
import ch.ethz.seb.sebserver.gbl.model.Domain.LMS_SETUP;
import ch.ethz.seb.sebserver.gbl.Constants; import ch.ethz.seb.sebserver.gbl.model.exam.Chapters;
import ch.ethz.seb.sebserver.gbl.api.APIMessage; import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.async.AsyncService; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.client.ClientCredentials; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
import ch.ethz.seb.sebserver.gbl.model.Domain.LMS_SETUP; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.model.exam.Chapters; import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam; import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData; import ch.ethz.seb.sebserver.webservice.WebserviceInfo;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.CourseAccessAPI;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails;
import ch.ethz.seb.sebserver.gbl.util.Result; public class MockCourseAccessAPI implements CourseAccessAPI {
import ch.ethz.seb.sebserver.webservice.WebserviceInfo;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; private final Collection<QuizData> mockups;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier; private final WebserviceInfo webserviceInfo;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService; private final APITemplateDataSupplier apiTemplateDataSupplier;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.AbstractCourseAccess; public MockCourseAccessAPI(
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.NoSEBRestrictionException; final APITemplateDataSupplier apiTemplateDataSupplier,
final WebserviceInfo webserviceInfo) {
public class MockupLmsAPITemplate implements LmsAPITemplate {
this.apiTemplateDataSupplier = apiTemplateDataSupplier;
private static final Logger log = LoggerFactory.getLogger(MockupLmsAPITemplate.class); this.webserviceInfo = webserviceInfo;
this.mockups = new ArrayList<>();
private final Collection<QuizData> mockups;
private final WebserviceInfo webserviceInfo; final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup();
private final APITemplateDataSupplier apiTemplateDataSupplier; final Long lmsSetupId = lmsSetup.id;
final Long institutionId = lmsSetup.getInstitutionId();
private final AbstractCourseAccess abstractCourseAccess; final LmsType lmsType = lmsSetup.getLmsType();
MockupLmsAPITemplate( this.mockups.add(new QuizData(
final AsyncService asyncService, "quiz1", institutionId, lmsSetupId, lmsType, "Demo Quiz 1 (MOCKUP)", "<p>Demo Quiz Mockup</p>",
final Environment environment, "2020-01-01T09:00:00Z", null, "http://lms.mockup.com/api/"));
final APITemplateDataSupplier apiTemplateDataSupplier, this.mockups.add(new QuizData(
final WebserviceInfo webserviceInfo) { "quiz2", institutionId, lmsSetupId, lmsType, "Demo Quiz 2 (MOCKUP)", "<p>Demo Quiz Mockup</p>",
"2020-01-01T09:00:00Z", "2025-01-01T09:00:00Z", "http://lms.mockup.com/api/"));
this.apiTemplateDataSupplier = apiTemplateDataSupplier; this.mockups.add(new QuizData(
this.webserviceInfo = webserviceInfo; "quiz3", institutionId, lmsSetupId, lmsType, "Demo Quiz 3 (MOCKUP)", "<p>Demo Quiz Mockup</p>",
this.mockups = new ArrayList<>(); "2018-07-30T09:00:00Z", "2018-08-01T00:00:00Z", "http://lms.mockup.com/api/"));
this.mockups.add(new QuizData(
this.abstractCourseAccess = new AbstractCourseAccess(asyncService, environment) { "quiz4", institutionId, lmsSetupId, lmsType, "Demo Quiz 4 (MOCKUP)", "<p>Demo Quiz Mockup</p>",
"2018-01-01T00:00:00Z", "2025-01-01T00:00:00Z", "http://lms.mockup.com/api/"));
@Override this.mockups.add(new QuizData(
protected Supplier<ExamineeAccountDetails> accountDetailsSupplier(final String examineeSessionId) { "quiz5", institutionId, lmsSetupId, lmsType, "Demo Quiz 5 (MOCKUP)", "<p>Demo Quiz Mockup</p>",
return () -> MockupLmsAPITemplate.this "2018-01-01T09:00:00Z", "2025-01-01T09:00:00Z", "http://lms.mockup.com/api/"));
.getExamineeAccountDetails_protected(examineeSessionId) this.mockups.add(new QuizData(
.getOrThrow(); "quiz6", institutionId, lmsSetupId, lmsType, "Demo Quiz 6 (MOCKUP)", "<p>Demo Quiz Mockup</p>",
} "2019-01-01T09:00:00Z", "2025-01-01T09:00:00Z", "http://lms.mockup.com/api/"));
this.mockups.add(new QuizData(
@Override "quiz7", institutionId, lmsSetupId, lmsType, "Demo Quiz 7 (MOCKUP)", "<p>Demo Quiz Mockup</p>",
protected Supplier<List<QuizData>> allQuizzesSupplier(final FilterMap filterMap) { "2018-01-01T09:00:00Z", "2025-01-01T09:00:00Z", "http://lms.mockup.com/api/"));
return () -> MockupLmsAPITemplate.this.getQuizzes_protected(filterMap).getOrThrow();
} this.mockups.add(new QuizData(
"quiz10", institutionId, lmsSetupId, lmsType, "Demo Quiz 10 (MOCKUP)",
@Override "Starts in a minute and ends after five minutes",
protected Supplier<Collection<QuizData>> quizzesSupplier(final Set<String> ids) { DateTime.now(DateTimeZone.UTC).plus(Constants.MINUTE_IN_MILLIS)
return () -> MockupLmsAPITemplate.this.getQuizzes_protected(ids).getOrThrow(); .toString(Constants.DEFAULT_DATE_TIME_FORMAT),
} DateTime.now(DateTimeZone.UTC).plus(6 * Constants.MINUTE_IN_MILLIS)
.toString(Constants.DEFAULT_DATE_TIME_FORMAT),
@Override "http://lms.mockup.com/api/"));
protected Supplier<QuizData> quizSupplier(final String id) { }
return () -> MockupLmsAPITemplate.this.getQuiz_protected(id).getOrThrow();
} @Override
public LmsSetupTestResult testCourseAccessAPI() {
@Override log.info("Test Lms Binding for Mockup and LmsSetup: {}", this.apiTemplateDataSupplier.getLmsSetup());
protected Supplier<Chapters> getCourseChaptersSupplier(final String courseId) {
throw new UnsupportedOperationException("Course Chapter feature not supported"); final List<APIMessage> missingAttrs = checkAttributes();
}
if (!missingAttrs.isEmpty()) {
}; return LmsSetupTestResult.ofMissingAttributes(LmsType.MOCKUP, missingAttrs);
}
final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup();
final Long lmsSetupId = lmsSetup.id; if (authenticate()) {
final Long institutionId = lmsSetup.getInstitutionId(); return LmsSetupTestResult.ofOkay(LmsType.MOCKUP);
final LmsType lmsType = lmsSetup.getLmsType(); } else {
return LmsSetupTestResult.ofTokenRequestError(LmsType.MOCKUP, "Illegal access");
this.mockups.add(new QuizData( }
"quiz1", institutionId, lmsSetupId, lmsType, "Demo Quiz 1 (MOCKUP)", "<p>Demo Quiz Mockup</p>", }
"2020-01-01T09:00:00Z", null, "http://lms.mockup.com/api/"));
this.mockups.add(new QuizData( private List<APIMessage> checkAttributes() {
"quiz2", institutionId, lmsSetupId, lmsType, "Demo Quiz 2 (MOCKUP)", "<p>Demo Quiz Mockup</p>", final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup();
"2020-01-01T09:00:00Z", "2025-01-01T09:00:00Z", "http://lms.mockup.com/api/")); final ClientCredentials lmsClientCredentials = this.apiTemplateDataSupplier.getLmsClientCredentials();
this.mockups.add(new QuizData( final List<APIMessage> missingAttrs = new ArrayList<>();
"quiz3", institutionId, lmsSetupId, lmsType, "Demo Quiz 3 (MOCKUP)", "<p>Demo Quiz Mockup</p>", if (StringUtils.isBlank(lmsSetup.lmsApiUrl)) {
"2018-07-30T09:00:00Z", "2018-08-01T00:00:00Z", "http://lms.mockup.com/api/")); missingAttrs.add(APIMessage.fieldValidationError(
this.mockups.add(new QuizData( LMS_SETUP.ATTR_LMS_URL,
"quiz4", institutionId, lmsSetupId, lmsType, "Demo Quiz 4 (MOCKUP)", "<p>Demo Quiz Mockup</p>", "lmsSetup:lmsUrl:notNull"));
"2018-01-01T00:00:00Z", "2025-01-01T00:00:00Z", "http://lms.mockup.com/api/")); }
this.mockups.add(new QuizData( if (!lmsClientCredentials.hasClientId()) {
"quiz5", institutionId, lmsSetupId, lmsType, "Demo Quiz 5 (MOCKUP)", "<p>Demo Quiz Mockup</p>", missingAttrs.add(APIMessage.fieldValidationError(
"2018-01-01T09:00:00Z", "2025-01-01T09:00:00Z", "http://lms.mockup.com/api/")); LMS_SETUP.ATTR_LMS_CLIENTNAME,
this.mockups.add(new QuizData( "lmsSetup:lmsClientname:notNull"));
"quiz6", institutionId, lmsSetupId, lmsType, "Demo Quiz 6 (MOCKUP)", "<p>Demo Quiz Mockup</p>", }
"2019-01-01T09:00:00Z", "2025-01-01T09:00:00Z", "http://lms.mockup.com/api/")); if (!lmsClientCredentials.hasSecret()) {
this.mockups.add(new QuizData( missingAttrs.add(APIMessage.fieldValidationError(
"quiz7", institutionId, lmsSetupId, lmsType, "Demo Quiz 7 (MOCKUP)", "<p>Demo Quiz Mockup</p>", LMS_SETUP.ATTR_LMS_CLIENTSECRET,
"2018-01-01T09:00:00Z", "2025-01-01T09:00:00Z", "http://lms.mockup.com/api/")); "lmsSetup:lmsClientsecret:notNull"));
}
this.mockups.add(new QuizData( return missingAttrs;
"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) @Override
.toString(Constants.DEFAULT_DATE_TIME_FORMAT), public Result<List<QuizData>> getQuizzes(final FilterMap filterMap) {
DateTime.now(DateTimeZone.UTC).plus(6 * Constants.MINUTE_IN_MILLIS) return Result.tryCatch(() -> {
.toString(Constants.DEFAULT_DATE_TIME_FORMAT), if (!authenticate()) {
"http://lms.mockup.com/api/")); throw new IllegalArgumentException("Wrong clientId or secret");
} }
@Override return this.mockups
public LmsType getType() { .stream()
return LmsType.MOCKUP; .map(this::getExternalAddressAlias)
} .filter(LmsAPIService.quizFilterPredicate(filterMap))
.collect(Collectors.toList());
@Override });
public LmsSetup lmsSetup() { }
return this.apiTemplateDataSupplier.getLmsSetup();
} @Override
public Result<Collection<QuizData>> getQuizzes(final Set<String> ids) {
private List<APIMessage> checkAttributes() { return Result.tryCatch(() -> {
final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup(); if (!authenticate()) {
final ClientCredentials lmsClientCredentials = this.apiTemplateDataSupplier.getLmsClientCredentials(); throw new IllegalArgumentException("Wrong clientId or secret");
final List<APIMessage> missingAttrs = new ArrayList<>(); }
if (StringUtils.isBlank(lmsSetup.lmsApiUrl)) {
missingAttrs.add(APIMessage.fieldValidationError( return this.mockups
LMS_SETUP.ATTR_LMS_URL, .stream()
"lmsSetup:lmsUrl:notNull")); .map(this::getExternalAddressAlias)
} .filter(mock -> ids.contains(mock.id))
if (!lmsClientCredentials.hasClientId()) { .collect(Collectors.toList());
missingAttrs.add(APIMessage.fieldValidationError( });
LMS_SETUP.ATTR_LMS_CLIENTNAME, }
"lmsSetup:lmsClientname:notNull"));
} @Override
if (!lmsClientCredentials.hasSecret()) { public Result<QuizData> getQuiz(final String id) {
missingAttrs.add(APIMessage.fieldValidationError( return Result.of(this.mockups
LMS_SETUP.ATTR_LMS_CLIENTSECRET, .stream()
"lmsSetup:lmsClientsecret:notNull")); .filter(q -> id.equals(q.id))
} .findFirst()
return missingAttrs; .get());
} }
@Override @Override
public LmsSetupTestResult testCourseAccessAPI() { public void clearCourseCache() {
log.info("Test Lms Binding for Mockup and LmsSetup: {}", this.apiTemplateDataSupplier.getLmsSetup()); // No cache here
}
final List<APIMessage> missingAttrs = checkAttributes();
@Override
if (!missingAttrs.isEmpty()) { public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeUserId) {
return LmsSetupTestResult.ofMissingAttributes(LmsType.MOCKUP, missingAttrs); return Result.ofError(new UnsupportedOperationException());
} }
if (authenticate()) { @Override
return LmsSetupTestResult.ofOkay(LmsType.MOCKUP); public String getExamineeName(final String examineeUserId) {
} else { return "--" + " (" + examineeUserId + ")";
return LmsSetupTestResult.ofTokenRequestError(LmsType.MOCKUP, "Illegal access"); }
}
} @Override
public Result<Chapters> getCourseChapters(final String courseId) {
@Override return Result.ofError(new UnsupportedOperationException("Course Chapter feature not supported"));
public LmsSetupTestResult testCourseRestrictionAPI() { }
return LmsSetupTestResult.ofQuizRestrictionAPIError(LmsType.MOCKUP, "unsupported");
} private boolean authenticate() {
try {
@Override
public Result<List<QuizData>> getQuizzes(final FilterMap filterMap) { final CharSequence plainClientId = this.apiTemplateDataSupplier.getLmsClientCredentials().clientId;
return this.abstractCourseAccess.protectedQuizzesRequest(filterMap); if (plainClientId == null || plainClientId.length() <= 0) {
} throw new IllegalAccessException("Wrong client credential");
}
private Result<List<QuizData>> getQuizzes_protected(final FilterMap filterMap) {
return Result.tryCatch(() -> { return true;
if (!authenticate()) { } catch (final Exception e) {
throw new IllegalArgumentException("Wrong clientId or secret"); log.info("Authentication failed: ", e);
} return false;
}
return this.mockups }
.stream()
.map(this::getExternalAddressAlias) private QuizData getExternalAddressAlias(final QuizData quizData) {
.filter(LmsAPIService.quizFilterPredicate(filterMap)) final String externalAddressAlias = this.webserviceInfo.getLmsExternalAddressAlias("lms.mockup.com");
.collect(Collectors.toList()); if (StringUtils.isNoneBlank(externalAddressAlias)) {
}); try {
}
final String _externalStartURI =
@Override this.webserviceInfo.getHttpScheme() +
public Result<QuizData> getQuiz(final String id) { "://" + externalAddressAlias + "/api/";
return this.abstractCourseAccess.protectedQuizRequest(id);
} return new QuizData(
quizData.id, quizData.institutionId, quizData.lmsSetupId, quizData.lmsType,
private Result<QuizData> getQuiz_protected(final String id) { quizData.name, quizData.description, quizData.startTime,
return Result.of(this.mockups quizData.endTime, _externalStartURI, quizData.additionalAttributes);
.stream() } catch (final Exception e) {
.filter(q -> id.equals(q.id)) log.error("Failed to create external address from alias: ", e);
.findFirst() return quizData;
.get()); }
} } else {
return quizData;
@Override }
public Result<Collection<QuizData>> getQuizzes(final Set<String> ids) { }
return this.abstractCourseAccess.protectedQuizzesRequest(ids);
} }
private Result<Collection<QuizData>> getQuizzes_protected(final Set<String> 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<Chapters> getCourseChapters(final String courseId) {
return this.abstractCourseAccess.getCourseChapters(courseId);
}
@Override
public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeSessionId) {
return this.abstractCourseAccess.getExamineeAccountDetails(examineeSessionId);
}
private Result<ExamineeAccountDetails> getExamineeAccountDetails_protected(final String examineeSessionId) {
return Result.ofError(new UnsupportedOperationException());
}
@Override
public String getExamineeName(final String examineeSessionId) {
return "--" + " (" + examineeSessionId + ")";
}
@Override
public Result<SEBRestriction> getSEBClientRestriction(final Exam exam) {
log.info("Apply SEB Client restriction for Exam: {}", exam);
return Result.ofError(new NoSEBRestrictionException());
}
@Override
public Result<SEBRestriction> applySEBClientRestriction(
final String externalExamId,
final SEBRestriction sebRestrictionData) {
log.info("Apply SEB Client restriction: {}", sebRestrictionData);
return Result.of(sebRestrictionData);
}
@Override
public Result<Exam> 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;
}
}
}

View file

@ -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.APITemplateDataSupplier;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplateFactory; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplateFactory;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.LmsAPITemplateAdapter;
@Lazy @Lazy
@Service @Service
@ -47,11 +48,19 @@ public class MockLmsAPITemplateFactory implements LmsAPITemplateFactory {
@Override @Override
public Result<LmsAPITemplate> create(final APITemplateDataSupplier apiTemplateDataSupplier) { public Result<LmsAPITemplate> 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.asyncService,
this.environment, this.environment,
apiTemplateDataSupplier, apiTemplateDataSupplier,
this.webserviceInfo)); mockCourseAccessAPI,
mockSEBRestrictionAPI));
} }
} }

View file

@ -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<SEBRestriction> getSEBClientRestriction(final Exam exam) {
log.info("Apply SEB Client restriction for Exam: {}", exam);
return Result.ofError(new NoSEBRestrictionException());
}
@Override
public Result<SEBRestriction> applySEBClientRestriction(
final String externalExamId,
final SEBRestriction sebRestrictionData) {
log.info("Apply SEB Client restriction: {}", sebRestrictionData);
return Result.of(sebRestrictionData);
}
@Override
public Result<Exam> releaseSEBClientRestriction(final Exam exam) {
log.info("Release SEB Client restriction for Exam: {}", exam);
return Result.of(exam);
}
}

View file

@ -16,7 +16,6 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Supplier;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; 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.Constants;
import ch.ethz.seb.sebserver.gbl.api.JSONMapper; import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
import ch.ethz.seb.sebserver.gbl.async.AsyncService;
import ch.ethz.seb.sebserver.gbl.async.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.Chapters;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData; import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
@ -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.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier; 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.CourseDataShort;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleCourseDataAsyncLoader.CourseQuizShort; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleCourseDataAsyncLoader.CourseQuizShort;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory.MoodleAPIRestTemplate; 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. * background task if needed and return immediately to do not block the request.
* The planed Moodle integration on moodle side also defines an improved course access API. This will * The planed Moodle integration on moodle side also defines an improved course access API. This will
* possibly make this synchronous fetch strategy obsolete in the future. */ * possibly make this synchronous fetch strategy obsolete in the future. */
public class MoodleCourseAccess extends AbstractCourseAccess { public class MoodleCourseAccess implements CourseAccessAPI {
private static final long INITIAL_WAIT_TIME = 3 * Constants.SECOND_IN_MILLIS; 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 JSONMapper jsonMapper;
private final MoodleRestTemplateFactory moodleRestTemplateFactory; private final MoodleRestTemplateFactory moodleRestTemplateFactory;
private final MoodleCourseDataAsyncLoader moodleCourseDataAsyncLoader; private final MoodleCourseDataAsyncLoader moodleCourseDataAsyncLoader;
private final CircuitBreaker<List<QuizData>> allQuizzesRequest;
private final boolean prependShortCourseName; private final boolean prependShortCourseName;
private MoodleAPIRestTemplate restTemplate; private MoodleAPIRestTemplate restTemplate;
@ -103,28 +98,12 @@ public class MoodleCourseAccess extends AbstractCourseAccess {
final JSONMapper jsonMapper, final JSONMapper jsonMapper,
final MoodleRestTemplateFactory moodleRestTemplateFactory, final MoodleRestTemplateFactory moodleRestTemplateFactory,
final MoodleCourseDataAsyncLoader moodleCourseDataAsyncLoader, final MoodleCourseDataAsyncLoader moodleCourseDataAsyncLoader,
final AsyncService asyncService,
final Environment environment) { final Environment environment) {
super(asyncService, environment);
this.jsonMapper = jsonMapper; this.jsonMapper = jsonMapper;
this.moodleCourseDataAsyncLoader = moodleCourseDataAsyncLoader; this.moodleCourseDataAsyncLoader = moodleCourseDataAsyncLoader;
this.moodleRestTemplateFactory = moodleRestTemplateFactory; 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( this.prependShortCourseName = BooleanUtils.toBoolean(environment.getProperty(
"sebserver.webservice.lms.moodle.prependShortCourseName", "sebserver.webservice.lms.moodle.prependShortCourseName",
Constants.TRUE_STRING)); Constants.TRUE_STRING));
@ -135,67 +114,7 @@ public class MoodleCourseAccess extends AbstractCourseAccess {
} }
@Override @Override
protected Supplier<ExamineeAccountDetails> accountDetailsSupplier(final String examineeSessionId) { public LmsSetupTestResult testCourseAccessAPI() {
return () -> {
try {
final MoodleAPIRestTemplate template = getRestTemplate()
.getOrThrow();
final MultiValueMap<String, String> 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.<MoodleUserDetails[]> readValue(
userDetailsJSON,
new TypeReference<MoodleUserDetails[]>() {
});
if (userDetails == null || userDetails.length <= 0) {
throw new RuntimeException("No user details on Moodle API request");
}
final Map<String, String> 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() {
final LmsSetupTestResult attributesCheck = this.moodleRestTemplateFactory.test(); final LmsSetupTestResult attributesCheck = this.moodleRestTemplateFactory.test();
if (!attributesCheck.isOk()) { if (!attributesCheck.isOk()) {
return attributesCheck; return attributesCheck;
@ -223,7 +142,45 @@ public class MoodleCourseAccess extends AbstractCourseAccess {
return LmsSetupTestResult.ofOkay(LmsType.MOODLE); return LmsSetupTestResult.ofOkay(LmsType.MOODLE);
} }
public Result<QuizData> getQuizFromCache(final String id) { @Override
public Result<List<QuizData>> getQuizzes(final FilterMap filterMap) {
return Result.tryCatch(() -> getRestTemplate()
.map(template -> collectAllQuizzes(template, filterMap))
.getOr(Collections.emptyList()));
}
@Override
public Result<Collection<QuizData>> getQuizzes(final Set<String> ids) {
return Result.tryCatch(() -> {
final List<QuizData> cached = getCached();
final List<QuizData> available = (cached != null)
? cached
: Collections.emptyList();
final Map<String, QuizData> quizMapping = available
.stream()
.collect(Collectors.toMap(q -> q.id, Function.identity()));
if (!quizMapping.keySet().containsAll(ids)) {
final Map<String, QuizData> 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<QuizData> getQuiz(final String id) {
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
final Map<String, CourseDataShort> cachedCourseData = this.moodleCourseDataAsyncLoader final Map<String, CourseDataShort> cachedCourseData = this.moodleCourseDataAsyncLoader
@ -255,76 +212,93 @@ public class MoodleCourseAccess extends AbstractCourseAccess {
} }
// get from LMS in protected request // get from LMS in protected request
return super.protectedQuizRequest(id).getOrThrow();
});
}
public Result<Collection<QuizData>> getQuizzesFromCache(final Set<String> ids) {
return Result.tryCatch(() -> {
final List<QuizData> cached = getCached();
final List<QuizData> available = (cached != null)
? cached
: Collections.emptyList();
final Map<String, QuizData> quizMapping = available
.stream()
.collect(Collectors.toMap(q -> q.id, Function.identity()));
if (!quizMapping.keySet().containsAll(ids)) {
final Map<String, QuizData> 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<QuizData> quizSupplier(final String id) {
return () -> {
final Set<String> ids = Stream.of(id).collect(Collectors.toSet()); final Set<String> ids = Stream.of(id).collect(Collectors.toSet());
return getRestTemplate() return getRestTemplate()
.map(template -> getQuizzesForIds(template, ids)) .map(template -> getQuizzesForIds(template, ids))
.getOr(Collections.emptyList()) .getOr(Collections.emptyList())
.get(0); .get(0);
}; });
} }
@Override @Override
protected Supplier<Collection<QuizData>> quizzesSupplier(final Set<String> ids) { public void clearCourseCache() {
return () -> getRestTemplate() // TODO Auto-generated method stub
.map(template -> getQuizzesForIds(template, ids))
.getOr(Collections.emptyList());
} }
@Override @Override
protected Supplier<List<QuizData>> allQuizzesSupplier(final FilterMap filterMap) { public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeSessionId) {
return () -> getRestTemplate() return Result.tryCatch(() -> {
.map(template -> collectAllQuizzes(template, filterMap))
.getOr(Collections.emptyList()); final MoodleAPIRestTemplate template = getRestTemplate()
.getOrThrow();
final MultiValueMap<String, String> 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.<MoodleUserDetails[]> readValue(
userDetailsJSON,
new TypeReference<MoodleUserDetails[]>() {
});
if (userDetails == null || userDetails.length <= 0) {
throw new RuntimeException("No user details on Moodle API request");
}
final Map<String, String> 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 @Override
protected Supplier<Chapters> getCourseChaptersSupplier(final String courseId) { public String getExamineeName(final String examineeUserId) {
throw new UnsupportedOperationException("not available yet"); return getExamineeAccountDetails(examineeUserId)
.map(ExamineeAccountDetails::getDisplayName)
.onError(error -> log.warn("Failed to request user-name for ID: {}", error.getMessage(), error))
.getOr(examineeUserId);
} }
@Override @Override
protected FetchStatus getFetchStatus() { public Result<Chapters> getCourseChapters(final String courseId) {
if (this.allQuizzesRequest.getState() != State.CLOSED) { return Result.ofError(new UnsupportedOperationException("not available yet"));
return FetchStatus.FETCH_ERROR; }
}
@Override
public FetchStatus getFetchStatus() {
if (this.moodleCourseDataAsyncLoader.isRunning()) { if (this.moodleCourseDataAsyncLoader.isRunning()) {
return FetchStatus.ASYNC_FETCH_RUNNING; return FetchStatus.ASYNC_FETCH_RUNNING;
} }

View file

@ -22,10 +22,13 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import ch.ethz.seb.sebserver.gbl.api.JSONMapper; 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.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.LmsSetup.LmsType;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.util.Result; 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.NoSEBRestrictionException;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory.MoodleAPIRestTemplate; 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): * Delete all key (and remove restrictions):
* POST: * POST:
* http://yourmoodle.org/webservice/rest/server.php?wstoken={token}&moodlewsrestformat=json&wsfunction=seb_restriction_delete&courseId=123 */ * 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); private static final Logger log = LoggerFactory.getLogger(MoodleCourseRestriction.class);
@ -84,7 +87,8 @@ public class MoodleCourseRestriction {
this.moodleRestTemplateFactory = moodleRestTemplateFactory; this.moodleRestTemplateFactory = moodleRestTemplateFactory;
} }
LmsSetupTestResult initAPIAccess() { @Override
public LmsSetupTestResult testCourseRestrictionAPI() {
// try to call the SEB Restrictions API // try to call the SEB Restrictions API
try { try {
@ -108,18 +112,35 @@ public class MoodleCourseRestriction {
return LmsSetupTestResult.ofOkay(LmsType.MOODLE); return LmsSetupTestResult.ofOkay(LmsType.MOODLE);
} }
Result<MoodleSEBRestriction> getSEBRestriction( @Override
final String internalId) { public Result<SEBRestriction> getSEBClientRestriction(final Exam exam) {
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
return getSEBRestriction( return getSEBRestriction(
MoodleCourseAccess.getQuizId(internalId), MoodleCourseAccess.getQuizId(exam.externalId),
MoodleCourseAccess.getShortname(internalId), MoodleCourseAccess.getShortname(exam.externalId),
MoodleCourseAccess.getIdnumber(internalId)) MoodleCourseAccess.getIdnumber(exam.externalId))
.map(restriction -> SEBRestriction.from(exam.id, restriction))
.getOrThrow(); .getOrThrow();
}); });
} }
@Override
public Result<SEBRestriction> applySEBClientRestriction(
final String externalExamId,
final SEBRestriction sebRestrictionData) {
return this.updateSEBRestriction(
externalExamId,
MoodleSEBRestriction.from(sebRestrictionData))
.map(result -> sebRestrictionData);
}
@Override
public Result<Exam> releaseSEBClientRestriction(final Exam exam) {
return this.deleteSEBRestriction(exam.externalId)
.map(result -> exam);
}
Result<MoodleSEBRestriction> getSEBRestriction( Result<MoodleSEBRestriction> getSEBRestriction(
final String quizId, final String quizId,
final String shortname, final String shortname,

View file

@ -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<List<QuizData>> getQuizzes(final FilterMap filterMap) {
return this.moodleCourseAccess
.protectedQuizzesRequest(filterMap)
.map(quizzes -> quizzes.stream()
.filter(LmsAPIService.quizFilterPredicate(filterMap))
.collect(Collectors.toList()));
}
@Override
public Result<QuizData> getQuiz(final String id) {
return this.moodleCourseAccess.getQuizFromCache(id);
}
@Override
public Result<Collection<QuizData>> getQuizzes(final Set<String> ids) {
return this.moodleCourseAccess.getQuizzesFromCache(ids);
}
@Override
public void clearCache() {
this.moodleCourseAccess.clearCache();
}
@Override
public Result<Chapters> getCourseChapters(final String courseId) {
return Result.tryCatch(() -> this.moodleCourseAccess
.getCourseChaptersSupplier(courseId)
.get());
}
@Override
public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeSessionId) {
return this.moodleCourseAccess.getExamineeAccountDetails(examineeSessionId);
}
@Override
public String getExamineeName(final String examineeSessionId) {
return this.moodleCourseAccess.getExamineeName(examineeSessionId);
}
@Override
public Result<SEBRestriction> 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<SEBRestriction> 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<Exam> 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);
}
}

View file

@ -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.APITemplateDataSupplier;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplateFactory; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplateFactory;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.LmsAPITemplateAdapter;
@Lazy @Lazy
@Service @Service
@ -88,14 +89,16 @@ public class MoodleLmsAPITemplateFactory implements LmsAPITemplateFactory {
this.jsonMapper, this.jsonMapper,
moodleRestTemplateFactory, moodleRestTemplateFactory,
asyncLoaderPrototype, asyncLoaderPrototype,
this.asyncService,
this.environment); this.environment);
final MoodleCourseRestriction moodleCourseRestriction = new MoodleCourseRestriction( final MoodleCourseRestriction moodleCourseRestriction = new MoodleCourseRestriction(
this.jsonMapper, this.jsonMapper,
moodleRestTemplateFactory); moodleRestTemplateFactory);
return new MoodleLmsAPITemplate( return new LmsAPITemplateAdapter(
this.asyncService,
this.environment,
apiTemplateDataSupplier,
moodleCourseAccess, moodleCourseAccess,
moodleCourseRestriction); moodleCourseRestriction);
}); });

View file

@ -14,8 +14,8 @@ import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
@ -24,7 +24,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.cache.CacheManager; import org.springframework.cache.CacheManager;
import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpEntity; import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
@ -35,7 +34,6 @@ import org.springframework.web.client.RestTemplate;
import ch.ethz.seb.sebserver.ClientHttpRequestFactoryService; import ch.ethz.seb.sebserver.ClientHttpRequestFactoryService;
import ch.ethz.seb.sebserver.gbl.api.APIMessage; 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.ClientCredentialService;
import ch.ethz.seb.sebserver.gbl.client.ClientCredentials; import ch.ethz.seb.sebserver.gbl.client.ClientCredentials;
import ch.ethz.seb.sebserver.gbl.client.ProxyData; import ch.ethz.seb.sebserver.gbl.client.ProxyData;
@ -75,11 +73,9 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
final ClientHttpRequestFactoryService clientHttpRequestFactoryService, final ClientHttpRequestFactoryService clientHttpRequestFactoryService,
final ClientCredentialService clientCredentialService, final ClientCredentialService clientCredentialService,
final APITemplateDataSupplier apiTemplateDataSupplier, final APITemplateDataSupplier apiTemplateDataSupplier,
final AsyncService asyncService,
final Environment environment,
final CacheManager cacheManager) { final CacheManager cacheManager) {
super(asyncService, environment, cacheManager); super(cacheManager);
this.clientHttpRequestFactoryService = clientHttpRequestFactoryService; this.clientHttpRequestFactoryService = clientHttpRequestFactoryService;
this.clientCredentialService = clientCredentialService; this.clientCredentialService = clientCredentialService;
@ -166,7 +162,7 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
@Override @Override
public Result<List<QuizData>> getQuizzes(final FilterMap filterMap) { public Result<List<QuizData>> getQuizzes(final FilterMap filterMap) {
return this return this
.protectedQuizzesRequest(filterMap) .allQuizzesRequest(filterMap)
.map(quizzes -> quizzes.stream() .map(quizzes -> quizzes.stream()
.filter(LmsAPIService.quizFilterPredicate(filterMap)) .filter(LmsAPIService.quizFilterPredicate(filterMap))
.collect(Collectors.toList())); .collect(Collectors.toList()));
@ -187,7 +183,7 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
}); });
if (!leftIds.isEmpty()) { if (!leftIds.isEmpty()) {
result.addAll(super.protectedQuizzesRequest(leftIds).getOrThrow()); result.addAll(quizzesRequest(leftIds).getOrThrow());
} }
return result; return result;
@ -201,18 +197,35 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
return Result.of(fromCache); return Result.of(fromCache);
} }
return super.protectedQuizRequest(id); return quizRequest(id);
} }
@Override @Override
protected Supplier<List<QuizData>> allQuizzesSupplier(final FilterMap filterMap) { public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeUserId) {
return () -> { 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<Chapters> getCourseChapters(final String courseId) {
return Result.ofError(new UnsupportedOperationException("No Course Chapter available for OpenOLAT LMS"));
}
protected Result<List<QuizData>> allQuizzesRequest(final FilterMap filterMap) {
return Result.tryCatch(() -> {
final List<QuizData> res = getRestTemplate() final List<QuizData> res = getRestTemplate()
.map(t -> this.collectAllQuizzes(t, filterMap)) .map(t -> this.collectAllQuizzes(t, filterMap))
.getOrThrow(); .getOrThrow();
super.putToCache(res); super.putToCache(res);
return res; return res;
}; });
} }
private String examUrl(final long olatRepositoryId) { private String examUrl(final long olatRepositoryId) {
@ -254,16 +267,15 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
@Override protected Result<Collection<QuizData>> quizzesRequest(final Set<String> ids) {
protected Supplier<Collection<QuizData>> quizzesSupplier(final Set<String> ids) { return Result.tryCatch(() -> ids.stream()
return () -> ids.stream().map(id -> quizSupplier(id).get()).collect(Collectors.toList()); .map(id -> quizRequest(id).getOr(null))
.filter(Objects::nonNull)
.collect(Collectors.toList()));
} }
@Override protected Result<QuizData> quizRequest(final String id) {
protected Supplier<QuizData> quizSupplier(final String id) { return getRestTemplate().map(t -> this.quizById(t, id));
return () -> getRestTemplate()
.map(t -> this.quizById(t, id))
.getOrThrow();
} }
private QuizData quizById(final OlatLmsRestTemplate restTemplate, final String id) { private QuizData quizById(final OlatLmsRestTemplate restTemplate, final String id) {
@ -295,20 +307,6 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
attrs); attrs);
} }
@Override
protected Supplier<ExamineeAccountDetails> accountDetailsSupplier(final String id) {
return () -> getRestTemplate()
.map(t -> this.getExamineeById(t, id))
.getOrThrow();
}
@Override
protected Supplier<Chapters> getCourseChaptersSupplier(final String courseId) {
return () -> {
throw new UnsupportedOperationException("No Course Chapter available for OpenOLAT LMS");
};
}
private SEBRestriction getRestrictionForAssignmentId(final RestTemplate restTemplate, final String id) { private SEBRestriction getRestrictionForAssignmentId(final RestTemplate restTemplate, final String id) {
final String url = String.format("/restapi/assessment_modes/%s/seb_restriction", id); final String url = String.format("/restapi/assessment_modes/%s/seb_restriction", id);
final RestrictionData r = this.apiGet(restTemplate, url, RestrictionData.class); final RestrictionData r = this.apiGet(restTemplate, url, RestrictionData.class);

View file

@ -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.APITemplateDataSupplier;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplateFactory; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplateFactory;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.LmsAPITemplateAdapter;
@Lazy @Lazy
@Service @Service
@ -63,13 +64,17 @@ public class OlatLmsAPITemplateFactory implements LmsAPITemplateFactory {
@Override @Override
public Result<LmsAPITemplate> create(final APITemplateDataSupplier apiTemplateDataSupplier) { public Result<LmsAPITemplate> create(final APITemplateDataSupplier apiTemplateDataSupplier) {
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
return new OlatLmsAPITemplate( final OlatLmsAPITemplate olatLmsAPITemplate = new OlatLmsAPITemplate(
this.clientHttpRequestFactoryService, this.clientHttpRequestFactoryService,
this.clientCredentialService, this.clientCredentialService,
apiTemplateDataSupplier, apiTemplateDataSupplier,
this.cacheManager);
return new LmsAPITemplateAdapter(
this.asyncService, this.asyncService,
this.environment, this.environment,
this.cacheManager); apiTemplateDataSupplier,
olatLmsAPITemplate,
olatLmsAPITemplate);
}); });
} }

View file

@ -11,7 +11,7 @@ logging.level.ROOT=INFO
logging.level.ch=INFO logging.level.ch=INFO
logging.level.ch.ethz.seb.sebserver.webservice.datalayer=INFO logging.level.ch.ethz.seb.sebserver.webservice.datalayer=INFO
logging.level.org.springframework.cache=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=DEBUG
logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.proctoring=INFO logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.proctoring=INFO
logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.indicator=DEBUG logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.indicator=DEBUG

View file

@ -22,8 +22,6 @@ import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap; import org.springframework.util.MultiValueMap;
import ch.ethz.seb.sebserver.gbl.api.JSONMapper; 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.LmsSetup.LmsType;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult.ErrorType; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult.ErrorType;
@ -75,7 +73,6 @@ public class MoodleCourseAccessTest {
new JSONMapper(), new JSONMapper(),
moodleRestTemplateFactory, moodleRestTemplateFactory,
null, null,
new AsyncService(new AsyncRunner()),
this.env); this.env);
final String examId = "123"; final String examId = "123";
@ -123,10 +120,9 @@ public class MoodleCourseAccessTest {
new JSONMapper(), new JSONMapper(),
moodleRestTemplateFactory, moodleRestTemplateFactory,
null, null,
mock(AsyncService.class),
this.env); this.env);
final LmsSetupTestResult initAPIAccess = moodleCourseAccess.initAPIAccess(); final LmsSetupTestResult initAPIAccess = moodleCourseAccess.testCourseAccessAPI();
assertNotNull(initAPIAccess); assertNotNull(initAPIAccess);
assertFalse(initAPIAccess.errors.isEmpty()); assertFalse(initAPIAccess.errors.isEmpty());
assertTrue(initAPIAccess.hasError(ErrorType.TOKEN_REQUEST)); assertTrue(initAPIAccess.hasError(ErrorType.TOKEN_REQUEST));
@ -145,10 +141,9 @@ public class MoodleCourseAccessTest {
new JSONMapper(), new JSONMapper(),
moodleRestTemplateFactory, moodleRestTemplateFactory,
null, null,
mock(AsyncService.class),
this.env); this.env);
final LmsSetupTestResult initAPIAccess = moodleCourseAccess.initAPIAccess(); final LmsSetupTestResult initAPIAccess = moodleCourseAccess.testCourseAccessAPI();
assertNotNull(initAPIAccess); assertNotNull(initAPIAccess);
assertFalse(initAPIAccess.errors.isEmpty()); assertFalse(initAPIAccess.errors.isEmpty());
assertTrue(initAPIAccess.hasError(ErrorType.QUIZ_ACCESS_API_REQUEST)); assertTrue(initAPIAccess.hasError(ErrorType.QUIZ_ACCESS_API_REQUEST));
@ -166,10 +161,9 @@ public class MoodleCourseAccessTest {
new JSONMapper(), new JSONMapper(),
moodleRestTemplateFactory, moodleRestTemplateFactory,
null, null,
mock(AsyncService.class),
this.env); this.env);
final LmsSetupTestResult initAPIAccess = moodleCourseAccess.initAPIAccess(); final LmsSetupTestResult initAPIAccess = moodleCourseAccess.testCourseAccessAPI();
assertNotNull(initAPIAccess); assertNotNull(initAPIAccess);
assertTrue(initAPIAccess.errors.isEmpty()); assertTrue(initAPIAccess.errors.isEmpty());