refactoring and improvement of LMS binding API

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,33 @@
/*
* Copyright (c) 2021 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.webservice.servicelayer.lms;
import ch.ethz.seb.sebserver.gbl.client.ClientCredentials;
import ch.ethz.seb.sebserver.gbl.client.ProxyData;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
/** Supplier for LmsAPITemplate to supply the templat with the needed LMS connection data. */
public interface APITemplateDataSupplier {
/** Get the LmsSetup instance containing all setup attributes
*
* @return the LmsSetup instance containing all setup attributes */
LmsSetup getLmsSetup();
/** Get the encoded LMS setup client credentials needed to access the LMS API.
*
* @return the encoded LMS setup client credentials needed to access the LMS API. */
ClientCredentials getLmsClientCredentials();
/** Get the proxy data if available and if needed for LMS connection
*
* @return the proxy data if available and if needed for LMS connection */
ProxyData getProxyData();
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,27 +0,0 @@
/*
* Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl;
import org.springframework.context.ApplicationEvent;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
public class LmsSetupChangeEvent extends ApplicationEvent {
private static final long serialVersionUID = -7239994198026689531L;
public LmsSetupChangeEvent(final LmsSetup source) {
super(source);
}
public LmsSetup getLmsSetup() {
return (LmsSetup) this.source;
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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