From dd6150ec2a3b62298f3d9b8009d293a1157ee4e5 Mon Sep 17 00:00:00 2001 From: anhefti Date: Fri, 14 May 2021 17:08:29 +0200 Subject: [PATCH] refactoring and improvement of LMS binding API --- .../ch/ethz/seb/sebserver/gbl/api/API.java | 1 + .../seb/sebserver/gbl/model/exam/Exam.java | 9 +- .../sebserver/gbl/model/exam/QuizData.java | 1 + .../gbl/model/institution/LmsSetup.java | 16 ++ .../seb/sebserver/gui/content/ExamForm.java | 2 + .../servicelayer/dao/LmsSetupDAO.java | 1 + .../servicelayer/dao/impl/ExamDAOImpl.java | 1 + .../servicelayer/exam/ExamAdminService.java | 36 ---- .../lms/APITemplateDataSupplier.java | 33 +++ .../servicelayer/lms/LmsAPIService.java | 3 + .../servicelayer/lms/LmsAPITemplate.java | 145 +++++++++---- .../lms/LmsAPITemplateFactory.java | 17 +- .../lms/SEBRestrictionService.java | 7 + ...eAccess.java => AbstractCourseAccess.java} | 6 +- .../lms/impl/LmsAPIServiceImpl.java | 185 +++++++++++------ .../lms/impl/LmsSetupChangeEvent.java | 27 --- .../lms/impl/SEBRestrictionServiceImpl.java | 66 ++++-- .../lms/impl/edx/OpenEdxCourseAccess.java | 85 ++++++-- .../impl/edx/OpenEdxCourseRestriction.java | 190 +++++++++++------- .../lms/impl/edx/OpenEdxLmsAPITemplate.java | 16 +- .../edx/OpenEdxLmsAPITemplateFactory.java | 16 +- .../impl/edx/OpenEdxRestTemplateFactory.java | 45 +++-- .../MockLmsAPITemplateFactory.java | 15 +- .../{ => mockup}/MockupLmsAPITemplate.java | 39 ++-- .../lms/impl/moodle/MoodleCourseAccess.java | 55 ++--- .../lms/impl/moodle/MoodleLmsAPITemplate.java | 13 +- .../moodle/MoodleLmsAPITemplateFactory.java | 22 +- .../moodle/MoodleRestTemplateFactory.java | 127 ++++++------ .../session/impl/ExamSessionServiceImpl.java | 39 +--- .../api/ExamAdministrationController.java | 71 ++++--- .../weblayer/api/LmsSetupController.java | 8 - .../gbl/model/ModelObjectJSONGenerator.java | 2 +- .../integration/UseCasesIntegrationTest.java | 1 + .../integration/api/admin/ExamAPITest.java | 2 + .../integration/api/admin/ExamImportTest.java | 28 ++- .../impl/moodle/MoodleCourseAccessTest.java | 4 - 36 files changed, 790 insertions(+), 544 deletions(-) create mode 100644 src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/APITemplateDataSupplier.java rename src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/{CourseAccess.java => AbstractCourseAccess.java} (98%) delete mode 100644 src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsSetupChangeEvent.java rename src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/{ => mockup}/MockLmsAPITemplateFactory.java (71%) rename src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/{ => mockup}/MockupLmsAPITemplate.java (88%) diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java b/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java index 110b2ce0..3f1a67a0 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java @@ -129,6 +129,7 @@ public final class API { public static final String EXAM_ADMINISTRATION_ENDPOINT = "/exam"; public static final String EXAM_ADMINISTRATION_DOWNLOAD_CONFIG_PATH_SEGMENT = "/download-config"; public static final String EXAM_ADMINISTRATION_CONSISTENCY_CHECK_PATH_SEGMENT = "/check-consistency"; + public static final String EXAM_ADMINISTRATION_CONSISTENCY_CHECK_INCLUDE_RESTRICTION = "include-restriction"; public static final String EXAM_ADMINISTRATION_SEB_RESTRICTION_PATH_SEGMENT = "/seb-restriction"; public static final String EXAM_ADMINISTRATION_CHECK_RESTRICTION_PATH_SEGMENT = "/check-seb-restriction"; public static final String EXAM_ADMINISTRATION_CHECK_IMPORTED_PATH_SEGMENT = "/check-imported"; diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java index d9a73d4c..ffb03ef8 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java @@ -47,7 +47,7 @@ public final class Exam implements GrantEntity { null, null, ExamStatus.FINISHED, -// Boolean.FALSE, + Boolean.FALSE, null, Boolean.FALSE, null); @@ -115,6 +115,9 @@ public final class Exam implements GrantEntity { @JsonProperty(EXAM.ATTR_STATUS) public final ExamStatus status; + @JsonProperty(EXAM.ATTR_LMS_SEB_RESTRICTION) + public final Boolean sebRestriction; + @JsonProperty(EXAM.ATTR_BROWSER_KEYS) public final String browserExamKeys; @@ -139,6 +142,7 @@ public final class Exam implements GrantEntity { @JsonProperty(EXAM.ATTR_OWNER) final String owner, @JsonProperty(EXAM.ATTR_SUPPORTER) final Collection supporter, @JsonProperty(EXAM.ATTR_STATUS) final ExamStatus status, + @JsonProperty(EXAM.ATTR_LMS_SEB_RESTRICTION) final Boolean sebRestriction, @JsonProperty(EXAM.ATTR_BROWSER_KEYS) final String browserExamKeys, @JsonProperty(EXAM.ATTR_ACTIVE) final Boolean active, @JsonProperty(EXAM.ATTR_LASTUPDATE) final String lastUpdate) { @@ -155,6 +159,7 @@ public final class Exam implements GrantEntity { this.type = type; this.owner = owner; this.status = (status != null) ? status : getStatusFromDate(startTime, endTime); + this.sebRestriction = sebRestriction; this.browserExamKeys = browserExamKeys; this.active = (active != null) ? active : Boolean.TRUE; this.lastUpdate = lastUpdate; @@ -181,6 +186,7 @@ public final class Exam implements GrantEntity { EXAM.ATTR_STATUS, ExamStatus.class, getStatusFromDate(this.startTime, this.endTime)); + this.sebRestriction = null; this.browserExamKeys = mapper.getString(EXAM.ATTR_BROWSER_KEYS); this.active = mapper.getBoolean(EXAM.ATTR_ACTIVE); this.supporter = mapper.getStringSet(EXAM.ATTR_SUPPORTER); @@ -204,6 +210,7 @@ public final class Exam implements GrantEntity { this.type = null; this.owner = null; this.status = (status != null) ? status : getStatusFromDate(this.startTime, this.endTime); + this.sebRestriction = null; this.browserExamKeys = null; this.active = null; this.supporter = null; diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/QuizData.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/QuizData.java index 474c880c..68ea3ac1 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/QuizData.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/QuizData.java @@ -29,6 +29,7 @@ import ch.ethz.seb.sebserver.gbl.util.Utils; public final class QuizData implements GrantEntity { + public static final String FILTER_ATTR_QUIZ_NAME = "quiz_name"; public static final String FILTER_ATTR_START_TIME = "start_timestamp"; public static final String ATTR_ADDITIONAL_ATTRIBUTES = "ADDITIONAL_ATTRIBUTES"; diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/LmsSetup.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/LmsSetup.java index 70a775b7..4a93f386 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/LmsSetup.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/LmsSetup.java @@ -37,16 +37,32 @@ public final class LmsSetup implements GrantEntity, Activatable { public static final String FILTER_ATTR_LMS_SETUP = "lms_setup"; public static final String FILTER_ATTR_LMS_TYPE = "lms_type"; + /** LMS binding and API features */ public enum Features { + /** The course API allows the application to securely connect to a LMS service + * and request course or quiz data from that LMS as well as requesting some + * limited LMS user account data like user name or display name. */ COURSE_API, + /** The SEB restriction API allows the application to securely connect to a LMS service + * and place or release SEB restrictions, for a particular course or quiz, on the LMS. + * The SEB restriciton is usually in the form of certain hash keys and addition + * restriction settings that prompt the LMS to check access on course/quiz connection and + * allow only access for a dedicated SEB client with the right configuration in place. */ SEB_RESTRICTION } + /** Defines the supported types if LMS bindings. + * Also defines the supports feature(s) for each type of LMS binding. */ public enum LmsType { + /** Mockup LMS type used to create test setups */ MOCKUP(Features.COURSE_API), + /** The Open edX LMS binding features both APIs, course access as well as SEB restrcition */ OPEN_EDX(Features.COURSE_API, Features.SEB_RESTRICTION), + /** The Moodle binding features only the course access API so far */ MOODLE(Features.COURSE_API /* , Features.SEB_RESTRICTION */), + /** The Ans Delft binding is on the way */ ANS_DELFT(/* Features.COURSE_API , Features.SEB_RESTRICTION */), + /** The OpenOLAT binding is on the way */ OPEN_OLAT(/* Features.COURSE_API , Features.SEB_RESTRICTION */); public final EnumSet features; diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamForm.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamForm.java index d7494bd6..ed8a7046 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamForm.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamForm.java @@ -226,6 +226,8 @@ public class ExamForm implements TemplateComposer { final ExamStatus examStatus = exam.getStatus(); final boolean editable = modifyGrant && (examStatus == ExamStatus.UP_COMING || examStatus == ExamStatus.RUNNING); + +// TODO this is not performat try to improve by doing one check with the CheckExamConsistency above final boolean sebRestrictionAvailable = testSEBRestrictionAPI(exam); final boolean isRestricted = readonly && sebRestrictionAvailable && this.restService .getBuilder(CheckSEBRestriction.class) diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/LmsSetupDAO.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/LmsSetupDAO.java index 771587f7..ffe0432e 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/LmsSetupDAO.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/LmsSetupDAO.java @@ -29,4 +29,5 @@ public interface LmsSetupDAO extends ActivatableEntityDAO, B * @param lmsSetupId the LMS Setup identifier * @return Result refer to the proxy data or to an error if happened */ Result getLmsAPIAccessProxyData(String lmsSetupId); + } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamDAOImpl.java index 0ed5636f..99ce6240 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamDAOImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamDAOImpl.java @@ -1010,6 +1010,7 @@ public class ExamDAOImpl implements ExamDAO { record.getOwner(), supporter, (quizData != null) ? status : (statusOverride != null) ? statusOverride : status, + BooleanUtils.toBooleanObject(record.getLmsSebRestriction()), record.getBrowserKeys(), BooleanUtils.toBooleanObject((quizData != null) ? record.getActive() : null), record.getLastupdate()); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamAdminService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamAdminService.java index f9480b8a..51314f64 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamAdminService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamAdminService.java @@ -41,17 +41,6 @@ public interface ExamAdminService { * @return Result refer to the restriction flag or to an error when happened */ Result isRestricted(Exam exam); -// /** Get the proctoring service settings for a certain exam to an error when happened. -// * -// * @param examId the exam instance -// * @return Result refer to proctoring service settings for the exam. */ -// default Result getProctoringServiceSettings(final Exam exam) { -// if (exam == null || exam.id == null) { -// return Result.ofRuntimeError("Invalid Exam model"); -// } -// return getProctoringServiceSettings(exam.id); -// } - /** Get proctoring service settings for a certain exam to an error when happened. * * @param examId the exam identifier @@ -99,29 +88,4 @@ public interface ExamAdminService { .flatMap(settings -> getExamProctoringService(settings.serverType)); } -// /** Get the exam proctoring service implementation of specified type. -// * -// * @param settings the ProctoringSettings that defines the ProctoringServerType -// * @return ExamProctoringService instance */ -// default Result getExamProctoringService(final ProctoringServiceSettings settings) { -// return Result.tryCatch(() -> getExamProctoringService(settings.serverType).getOrThrow()); -// } -// -// /** Get the exam proctoring service implementation for specified exam. -// * -// * @param exam the exam instance -// * @return ExamProctoringService instance */ -// default Result getExamProctoringService(final Exam exam) { -// return Result.tryCatch(() -> getExamProctoringService(exam.id).getOrThrow()); -// } -// -// /** Get the exam proctoring service implementation for specified exam. -// * -// * @param examId the exam identifier -// * @return ExamProctoringService instance */ -// default Result getExamProctoringService(final Long examId) { -// return getProctoringServiceSettings(examId) -// .flatMap(this::getExamProctoringService); -// } - } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/APITemplateDataSupplier.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/APITemplateDataSupplier.java new file mode 100644 index 00000000..5dad13a6 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/APITemplateDataSupplier.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2021 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package ch.ethz.seb.sebserver.webservice.servicelayer.lms; + +import ch.ethz.seb.sebserver.gbl.client.ClientCredentials; +import ch.ethz.seb.sebserver.gbl.client.ProxyData; +import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; + +/** Supplier for LmsAPITemplate to supply the templat with the needed LMS connection data. */ +public interface APITemplateDataSupplier { + + /** Get the LmsSetup instance containing all setup attributes + * + * @return the LmsSetup instance containing all setup attributes */ + LmsSetup getLmsSetup(); + + /** Get the encoded LMS setup client credentials needed to access the LMS API. + * + * @return the encoded LMS setup client credentials needed to access the LMS API. */ + ClientCredentials getLmsClientCredentials(); + + /** Get the proxy data if available and if needed for LMS connection + * + * @return the proxy data if available and if needed for LMS connection */ + ProxyData getProxyData(); + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/LmsAPIService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/LmsAPIService.java index 468e424a..3567428e 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/LmsAPIService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/LmsAPIService.java @@ -35,6 +35,9 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; * changes this service will be notifies about the change and release the related LmsAPITemplate from cache. */ public interface LmsAPIService { + /** Reset and cleanup the caches if there are some */ + void cleanup(); + /** Get the specified LmsSetup model by primary key * * @param id The identifier (PK) of the LmsSetup model diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/LmsAPITemplate.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/LmsAPITemplate.java index 2b556db4..aec35ad1 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/LmsAPITemplate.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/LmsAPITemplate.java @@ -17,6 +17,7 @@ import java.util.Set; import org.apache.commons.lang3.StringUtils; import ch.ethz.seb.sebserver.gbl.api.EntityType; +import ch.ethz.seb.sebserver.gbl.async.MemoizingCircuitBreaker; import ch.ethz.seb.sebserver.gbl.model.exam.Chapters; import ch.ethz.seb.sebserver.gbl.model.exam.Exam; import ch.ethz.seb.sebserver.gbl.model.exam.QuizData; @@ -27,64 +28,114 @@ import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails; import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ResourceNotFoundException; +import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.AbstractCourseAccess; /** Defines an LMS API access template to build SEB Server LMS integration. - * + *

* A LMS integration consists of two main parts so far: - * - The course API to search and request course data from LMS as well as resolve some LMS account details for a given - * examineeId - * - The SEB restriction API to apply SEB restriction data to the LMS to restrict a certain course for SEB + *

* - * A LmsAPITemplate is been constructed within a LmsSetup that defines the LMS setup data that is needed to connect to - * a specific LMS instance of implemented type. - * - * The enum LmsSetup.LmsType defines the supported LMS types and for each type the supported API part(s). - * - * SEB Server uses the test functions that are defined for each LMS API part to test API access for a certain LMS - * instance respectively the underling LMSSetup. Concrete implementations can do various tests to check full - * or partial API Access and can flag missing or wrong LMSSetup attributes with the resulting LmsSetupTestResult. + *
+ * - The course API to search and request course data from LMS as well as resolve some
+ *   LMS account details for a given examineeId.
+ * - The SEB restriction API to apply SEB restriction data to the LMS to restrict a
+ *   certain course for SEB.
+ * 
+ *

* + * Course API
+ * All course API requests of this template shall not block and return as fast as possible + * with the best result it can provide for the time on that the request was made. + *

+ * Since the course API requests course data from potentially thousands of existing and + * active courses, the course API can implement some caches if needed.
+ * A cache in the course API has manly two purposes; The first and prior purpose is to + * be able to provide course data as fast as possible even if the LMS is not available or + * busy at the time. The second purpose is to guarantee fast data access for the system + * if this is needed and data actuality has second priority.
+ * Therefore usual get quiz data functions like {@link #getQuizzes(FilterMap filterMap) }, + * {@link #getQuizzes(Set ids) } and {@link #getQuiz(final String id) } + * shall always first try to connect to the LMS and request the specified data from the LMS. + * If this succeeds the cache shall be updated with the received quizzes data and return them. + * If this is not possible within a certain time, the implementation shall get as much of the + * requested data from the cache and return them to the caller to not block the call too long + * and allow the caller to return fast and present as much data as possible.
+ * This can be done with a {@link MemoizingCircuitBreaker} or a simple {@link CircuitBreaker} + * with a separated cache, for example. The abstract implementation; {@link AbstractCourseAccess} + * provides already defined wrapped circuit breaker for each call. To use it, just extend the + * abstract class and implement the needed suppliers.
+ * On the other hand, dedicated cache access functions like {@link #getQuizzesFromCache(Set ids) } + * shall always first look into the cache to geht the requested data and if not + * available, call the LMS and request the data from the LMS. If partial data is needed to get + * be requested from the LMS, this functions shall also update the catch with the requested + * and cache missed data afterwards. + *

+ * SEB restriction API
+ * For this API we need no caching since this is mostly about pushing data to the LMS for the LMS + * to use. But this calls sahl also be protected within some kind of circuit breaker pattern to + * avoid blocking on long latency. + *

+ *

+ * A {@link LmsAPITemplate } will be constructed within the application with a {@link LmsSetup } instances. + * The application constructs a {@link LmsAPITemplate } for each type of LMS setup when needed or requested and + * there is not already a cached template or the cached template is out of date.
+ * The {@link LmsSetup } defines the data that is needed to connect to a specific LMS instance of implemented type + * and is wrapped within a {@link LmsAPITemplate } instance that lives as long as there are no changes to the + * {@link LmsSetup and the {@link LmsSetup } that is wrapped within the {@link LmsAPITemplate } is up to date. + *

+ * The enum {@link LmsSetup.LmsType } defines the supported LMS types and for each type the supported API part(s). + *

+ * The application uses the test functions that are defined for each LMS API part to test API access for a certain LMS + * instance respectively the underling {@link LmsSetup }. Concrete implementations can do various tests to check full + * or partial API Access and can flag missing or wrong {@link LmsSetup } attributes with the resulting + * {@link LmsSetupTestResult }.
* SEB Server than uses an instance of this template to communicate with the an LMS. */ public interface LmsAPITemplate { - /** Get the underling LMSSetup configuration for this LmsAPITemplate + /** Get the LMS type of the concrete template implementation * - * @return the underling LMSSetup configuration for this LmsAPITemplate */ + * @return the LMS type of the concrete template implementation */ + LmsSetup.LmsType getType(); + + /** Get the underling {@link LmsSetup } configuration for this LmsAPITemplate + * + * @return the underling {@link LmsSetup } configuration for this LmsAPITemplate */ LmsSetup lmsSetup(); - /** Performs a test for the underling LmsSetup configuration and checks if the + // ******************************************************************* + // **** Course API functions ***************************************** + + /** Performs a test for the underling {@link LmsSetup } configuration and checks if the * LMS and the course API of the LMS can be accessed or if there are some difficulties, * missing configuration data or connection/authentication errors. * - * @return LmsSetupTestResult instance with the test result report */ + * @return {@link LmsSetupTestResult } instance with the test result report */ LmsSetupTestResult testCourseAccessAPI(); - /** Performs a test for the underling LmsSetup configuration and checks if the - * LMS and the course restriction API of the LMS can be accessed or if there are some difficulties, - * missing configuration data or connection/authentication errors. + /** Get an unsorted List of filtered {@link QuizData } from the LMS course/quiz API * - * @return LmsSetupTestResult instance with the test result report */ - LmsSetupTestResult testCourseRestrictionAPI(); - - /** Get an unsorted List of filtered QuizData from the LMS course/quiz API + * @param filterMap the {@link FilterMap } to get a filtered result. Possible filter attributes are: * - * @param filterMap the FilterMap to get a filtered result. For possible filter attributes - * see documentation on QuizData - * @return Result of an unsorted List of filtered QuizData from the LMS course/quiz API + *

+     *      {@link QuizData.FILTER_ATTR_QUIZ_NAME } The quiz name filter text (exclude all names that do not contain the given text)
+     *      {@link QuizData.FILTER_ATTR_START_TIME } The quiz start time (exclude all quizzes that starts before)
+     *            
+ * + * @return Result of an unsorted List of filtered {@link QuizData } from the LMS course/quiz API * or refer to an error when happened */ Result> getQuizzes(FilterMap filterMap); - /** Get all QuizData for the set of QuizData identifiers from LMS API in a collection - * of Result. If particular Quiz cannot be loaded because of errors or deletion, + /** Get all {@link QuizData } for the set of {@link QuizData } identifiers from LMS API in a collection + * of Result. If particular quiz cannot be loaded because of errors or deletion, * the Result will have an error reference. * - * @param ids the Set of Quiz identifiers to get the QuizData for - * @return Collection of all QuizData from the given id set */ + * @param ids the Set of Quiz identifiers to get the {@link QuizData } for + * @return Collection of all {@link QuizData } from the given id set */ Collection> getQuizzes(Set ids); /** Get the quiz data with specified identifier. * - * Default implementation: Uses getQuizzes(Set ids) and returns the first matching or an error. + * Default implementation: Uses {@link #getQuizzes(Set ids) } and returns the first matching or an error. * * @param id the quiz data identifier * @return Result refer to the quiz data or to an error when happened */ @@ -99,26 +150,27 @@ public interface LmsAPITemplate { .orElse(Result.ofError(new ResourceNotFoundException(EntityType.EXAM, id))); } - /** Get all QuizData for the set of QuizData-identifiers (ids) from the LMS defined within the - * underling LMSSetup, in a collection of Results. + /** Get all {@link QuizData } for the set of {@link QuizData }-identifiers (ids) from the LMS defined within the + * underling LmsSetup, in a collection of Results. * * If there is caching involved this function shall try to get the data from the cache first. * * NOTE: This function depends on the specific LMS implementation and on whether caching the quiz data * makes sense or not. Following strategy is recommended: - * Looks first in the cache if the whole set of QuizData can be get from the cache. + * Looks first in the cache if the whole set of {@link QuizData } can be get from the cache. * If all quizzes are cached, returns all from cache. * If one or more quiz is not in the cache, requests all quizzes from the API and refreshes the cache * - * @param ids the Set of Quiz identifiers to get the QuizData for - * @return Collection of all QuizData from the given id set */ + * @param ids the Set of Quiz identifiers to get the {@link QuizData } for + * @return Collection of all {@link QuizData } from the given id set */ Collection> getQuizzesFromCache(Set ids); /** Convert an anonymous or temporary examineeUserId, sent by the SEB Client on LMS login, * to LMS examinee account details by requesting them on the LMS API with the given examineeUserId * * @param examineeUserId the examinee user identifier derived from SEB Client - * @return a Result refer to the ExamineeAccountDetails instance or to an error when happened or not supported */ + * @return a Result refer to the {@link ExamineeAccountDetails } instance or to an error when happened or not + * supported */ Result getExamineeAccountDetails(String examineeUserId); /** Used to convert an anonymous or temporary examineeUserId, sent by the SEB Client on LMS login, @@ -142,11 +194,23 @@ public interface LmsAPITemplate { * @return Result referencing to the Chapters model for the given course or to an error when happened. */ Result getCourseChapters(String courseId); - /** Get SEB restriction data form LMS within a SEBRestrictionData instance. The available restriction details + // **************************************************************************** + // **** SEB restriction API functions ***************************************** + + /** Performs a test for the underling {@link LmsSetup } configuration and checks if the + * LMS and the course restriction API of the LMS can be accessed or if there are some difficulties, + * missing configuration data or connection/authentication errors. + * + * @return {@link LmsSetupTestResult } instance with the test result report */ + LmsSetupTestResult testCourseRestrictionAPI(); + + /** Get SEB restriction data form LMS within a {@link SEBRestrictionData } instance. The available restriction + * details * depends on the type of LMS but shall at least contains the config-key(s) and the browser-exam-key(s). * * @param exam the exam to get the SEB restriction data for - * @return Result refer to the SEBRestrictionData instance or to an ResourceNotFoundException if the restriction is + * @return Result refer to the {@link SEBRestrictionData } instance or to an ResourceNotFoundException if the + * restriction is * missing or to another exception on unexpected error case */ Result getSEBClientRestriction(Exam exam); @@ -154,7 +218,8 @@ public interface LmsAPITemplate { * * @param externalExamId The exam/course identifier from LMS side (Exam.externalId) * @param sebRestrictionData containing all data for SEB Client restriction to apply to the LMS - * @return Result refer to the given SEBRestrictionData if restriction was successful or to an error if not */ + * @return Result refer to the given {@link SEBRestrictionData } if restriction was successful or to an error if + * not */ Result applySEBClientRestriction( String externalExamId, SEBRestriction sebRestrictionData); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/LmsAPITemplateFactory.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/LmsAPITemplateFactory.java index 0af397a8..518b1b74 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/LmsAPITemplateFactory.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/LmsAPITemplateFactory.java @@ -8,9 +8,6 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms; -import ch.ethz.seb.sebserver.gbl.client.ClientCredentials; -import ch.ethz.seb.sebserver.gbl.client.ProxyData; -import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType; import ch.ethz.seb.sebserver.gbl.util.Result; @@ -23,16 +20,10 @@ public interface LmsAPITemplateFactory { * @return the LMS type if a specific implementation */ LmsType lmsType(); - /** Creates a LmsAPITemplate for the specific implements LMS type. + /** Creates a {@link LmsAPITemplate } for the specific implements LMS type + * And provides it with the needed {@link APITemplateDataSupplier } * - * @param lmsSetup the LMS setup data to initialize the template - * @param credentials the access data for accessing the LMS API. Either client credentials or access token from LMS - * setup input - * @param proxyData The proxy data used to connect to the LMS if needed. - * @return Result refer to the LmsAPITemplate or to an error when happened */ - Result create( - final LmsSetup lmsSetup, - final ClientCredentials credentials, - final ProxyData proxyData); + * @param apiTemplateDataSupplier supplies all needed actual LMS setup data */ + Result create(final APITemplateDataSupplier apiTemplateDataSupplier); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/SEBRestrictionService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/SEBRestrictionService.java index 3cea6a00..57558816 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/SEBRestrictionService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/SEBRestrictionService.java @@ -8,6 +8,8 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms; +import javax.validation.constraints.NotNull; + import ch.ethz.seb.sebserver.gbl.model.exam.Exam; import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction; import ch.ethz.seb.sebserver.gbl.util.Result; @@ -19,6 +21,9 @@ public interface SEBRestrictionService { String SEB_RESTRICTION_ADDITIONAL_PROPERTY_CONFIG_KEY = "config_key"; + /** Get the LmsAPIService that is used by the SEBRestrictionService */ + LmsAPIService getLmsAPIService(); + /** Get the SEBRestriction properties for specified Exam. * * @param exam the Exam @@ -50,4 +55,6 @@ public interface SEBRestrictionService { * @return Result refer to the Exam instance or to an error if happened */ Result releaseSEBClientRestriction(Exam exam); + boolean checkConsistency(@NotNull Long lmsSetupId, Exam exam); + } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/CourseAccess.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/AbstractCourseAccess.java similarity index 98% rename from src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/CourseAccess.java rename to src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/AbstractCourseAccess.java index a4467257..b43fb874 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/CourseAccess.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/AbstractCourseAccess.java @@ -35,9 +35,9 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; * API requests in a protected environment. * * Extend this to implement a concrete course access API for a given type of LMS. */ -public abstract class CourseAccess { +public abstract class AbstractCourseAccess { - private static final Logger log = LoggerFactory.getLogger(CourseAccess.class); + private static final Logger log = LoggerFactory.getLogger(AbstractCourseAccess.class); /** Fetch status that indicates an asynchronous quiz data fetch status if the * concrete implementation has such. */ @@ -54,7 +54,7 @@ public abstract class CourseAccess { /** CircuitBreaker for protected examinee account details requests */ protected final CircuitBreaker accountDetailRequest; - protected CourseAccess( + protected AbstractCourseAccess( final AsyncService asyncService, final Environment environment) { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsAPIServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsAPIServiceImpl.java index df4728c4..407bcd40 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsAPIServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsAPIServiceImpl.java @@ -22,10 +22,10 @@ import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Lazy; -import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; import ch.ethz.seb.sebserver.gbl.Constants; +import ch.ethz.seb.sebserver.gbl.api.EntityType; import ch.ethz.seb.sebserver.gbl.client.ClientCredentialService; import ch.ethz.seb.sebserver.gbl.client.ClientCredentials; import ch.ethz.seb.sebserver.gbl.client.ProxyData; @@ -38,6 +38,8 @@ import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.LmsSetupDAO; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ResourceNotFoundException; +import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplateFactory; @@ -71,21 +73,9 @@ public class LmsAPIServiceImpl implements LmsAPIService { this.templateFactories = new EnumMap<>(factories); } - /** Listen to LmsSetupChangeEvent to release an affected LmsAPITemplate from cache - * - * @param event the event holding the changed LmsSetup */ - @EventListener - public void notifyLmsSetupChange(final LmsSetupChangeEvent event) { - final LmsSetup lmsSetup = event.getLmsSetup(); - if (lmsSetup == null) { - return; - } - - if (log.isDebugEnabled()) { - log.debug("LmsSetup changed. Update cache by removing eventually used references"); - } - - this.cache.remove(new CacheKey(lmsSetup.getModelId(), 0)); + @Override + public void cleanup() { + this.cache.clear(); } @Override @@ -107,10 +97,19 @@ public class LmsAPIServiceImpl implements LmsAPIService { @Override public Result getLmsAPITemplate(final String lmsSetupId) { - return Result.tryCatch(() -> this.lmsSetupDAO - .byModelId(lmsSetupId) - .getOrThrow()) - .flatMap(this::getLmsAPITemplate); + return Result.tryCatch(() -> { + LmsAPITemplate lmsAPITemplate = getFromCache(lmsSetupId); + if (lmsAPITemplate == null) { + lmsAPITemplate = createLmsSetupTemplate(lmsSetupId); + if (lmsAPITemplate != null) { + this.cache.put(new CacheKey(lmsSetupId, System.currentTimeMillis()), lmsAPITemplate); + } + } + if (lmsAPITemplate == null) { + throw new ResourceNotFoundException(EntityType.LMS_SETUP, lmsSetupId); + } + return lmsAPITemplate; + }); } @Override @@ -129,23 +128,22 @@ public class LmsAPIServiceImpl implements LmsAPIService { @Override public LmsSetupTestResult testAdHoc(final LmsSetup lmsSetup) { - final ClientCredentials lmsCredentials = this.clientCredentialService.encryptClientCredentials( - lmsSetup.lmsAuthName, - lmsSetup.lmsAuthSecret, - lmsSetup.lmsRestApiToken) - .getOrThrow(); + final AdHocAPITemplateDataSupplier apiTemplateDataSupplier = new AdHocAPITemplateDataSupplier( + lmsSetup, + this.clientCredentialService); - final ProxyData proxyData = (StringUtils.isNoneBlank(lmsSetup.proxyHost)) - ? new ProxyData( - lmsSetup.proxyHost, - lmsSetup.proxyPort, - this.clientCredentialService.encryptClientCredentials( - lmsSetup.proxyAuthUsername, - lmsSetup.proxyAuthSecret) - .getOrThrow()) - : null; + final LmsAPITemplate lmsSetupTemplate = createLmsSetupTemplate(apiTemplateDataSupplier); - return test(createLmsSetupTemplate(lmsSetup, lmsCredentials, proxyData)); + final LmsSetupTestResult testCourseAccessAPI = lmsSetupTemplate.testCourseAccessAPI(); + if (!testCourseAccessAPI.isOk()) { + return testCourseAccessAPI; + } + + if (lmsSetupTemplate.lmsSetup().getLmsType().features.contains(LmsSetup.Features.SEB_RESTRICTION)) { + return lmsSetupTemplate.testCourseRestrictionAPI(); + } + + return LmsSetupTestResult.ofOkay(); } /** Collect all QuizData from all affecting LmsSetup. @@ -183,6 +181,7 @@ public class LmsAPIServiceImpl implements LmsAPIService { return this.lmsSetupDAO.all(institutionId, true) .getOrThrow() .parallelStream() + .map(LmsSetup::getModelId) .map(this::getLmsAPITemplate) .flatMap(Result::onErrorLogAndSkip) .map(template -> template.getQuizzes(filterMap)) @@ -193,18 +192,7 @@ public class LmsAPIServiceImpl implements LmsAPIService { }); } - private Result getLmsAPITemplate(final LmsSetup lmsSetup) { - return Result.tryCatch(() -> { - LmsAPITemplate lmsAPITemplate = getFromCache(lmsSetup); - if (lmsAPITemplate == null) { - lmsAPITemplate = createLmsSetupTemplate(lmsSetup); - this.cache.put(new CacheKey(lmsSetup.getModelId(), System.currentTimeMillis()), lmsAPITemplate); - } - return lmsAPITemplate; - }); - } - - private LmsAPITemplate getFromCache(final LmsSetup lmsSetup) { + private LmsAPITemplate getFromCache(final String lmsSetupId) { // first cleanup the cache by removing old instances final long currentTimeMillis = System.currentTimeMillis(); new ArrayList<>(this.cache.keySet()) @@ -212,40 +200,103 @@ public class LmsAPIServiceImpl implements LmsAPIService { .filter(key -> key.creationTimestamp - currentTimeMillis > Constants.DAY_IN_MILLIS) .forEach(this.cache::remove); // get from cache - return this.cache.get(new CacheKey(lmsSetup.getModelId(), 0)); + return this.cache.get(new CacheKey(lmsSetupId, 0)); } - private LmsAPITemplate createLmsSetupTemplate(final LmsSetup lmsSetup) { + private LmsAPITemplate createLmsSetupTemplate(final String lmsSetupId) { if (log.isDebugEnabled()) { - log.debug("Create new LmsAPITemplate for id: {}", lmsSetup.getModelId()); + log.debug("Create new LmsAPITemplate for id: {}", lmsSetupId); } - final ClientCredentials credentials = this.lmsSetupDAO - .getLmsAPIAccessCredentials(lmsSetup.getModelId()) - .getOrThrow(); - - final ProxyData proxyData = this.lmsSetupDAO - .getLmsAPIAccessProxyData(lmsSetup.getModelId()) - .getOr(null); - - return createLmsSetupTemplate(lmsSetup, credentials, proxyData); + return createLmsSetupTemplate(new PersistentAPITemplateDataSupplier( + lmsSetupId, + this.lmsSetupDAO)); } - private LmsAPITemplate createLmsSetupTemplate( - final LmsSetup lmsSetup, - final ClientCredentials credentials, - final ProxyData proxyData) { + private LmsAPITemplate createLmsSetupTemplate(final APITemplateDataSupplier apiTemplateDataSupplier) { - if (!this.templateFactories.containsKey(lmsSetup.lmsType)) { - throw new UnsupportedOperationException("No support for LMS Type: " + lmsSetup.lmsType); + final LmsType lmsType = apiTemplateDataSupplier.getLmsSetup().lmsType; + + if (!this.templateFactories.containsKey(lmsType)) { + throw new UnsupportedOperationException("No support for LMS Type: " + lmsType); } - final LmsAPITemplateFactory lmsAPITemplateFactory = this.templateFactories.get(lmsSetup.lmsType); - return lmsAPITemplateFactory.create(lmsSetup, credentials, proxyData) + final LmsAPITemplateFactory lmsAPITemplateFactory = this.templateFactories + .get(lmsType); + + return lmsAPITemplateFactory + .create(apiTemplateDataSupplier) .getOrThrow(); } + /** Used to always get the actual LMS connection data from persistent */ + private static final class PersistentAPITemplateDataSupplier implements APITemplateDataSupplier { + + private final String lmsSetupId; + private final LmsSetupDAO lmsSetupDAO; + + public PersistentAPITemplateDataSupplier(final String lmsSetupId, final LmsSetupDAO lmsSetupDAO) { + this.lmsSetupId = lmsSetupId; + this.lmsSetupDAO = lmsSetupDAO; + } + + @Override + public LmsSetup getLmsSetup() { + return this.lmsSetupDAO.byModelId(this.lmsSetupId).getOrThrow(); + } + + @Override + public ClientCredentials getLmsClientCredentials() { + return this.lmsSetupDAO.getLmsAPIAccessCredentials(this.lmsSetupId).getOrThrow(); + } + + @Override + public ProxyData getProxyData() { + return this.lmsSetupDAO.getLmsAPIAccessProxyData(this.lmsSetupId).getOr(null); + } + } + + /** Used to test LMS connection data that are not yet persistently stored */ + private static final class AdHocAPITemplateDataSupplier implements APITemplateDataSupplier { + + private final LmsSetup lmsSetup; + private final ClientCredentialService clientCredentialService; + + public AdHocAPITemplateDataSupplier( + final LmsSetup lmsSetup, + final ClientCredentialService clientCredentialService) { + this.lmsSetup = lmsSetup; + this.clientCredentialService = clientCredentialService; + } + + @Override + public LmsSetup getLmsSetup() { + return this.lmsSetup; + } + + @Override + public ClientCredentials getLmsClientCredentials() { + return this.clientCredentialService.encryptClientCredentials( + this.lmsSetup.getLmsAuthName(), + this.lmsSetup.getLmsAuthSecret()) + .getOrThrow(); + } + + @Override + public ProxyData getProxyData() { + return (StringUtils.isNoneBlank(this.lmsSetup.proxyHost)) + ? new ProxyData( + this.lmsSetup.proxyHost, + this.lmsSetup.proxyPort, + this.clientCredentialService.encryptClientCredentials( + this.lmsSetup.proxyAuthUsername, + this.lmsSetup.proxyAuthSecret) + .getOrThrow()) + : null; + } + } + private static final class CacheKey { final String lmsSetupId; final long creationTimestamp; diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsSetupChangeEvent.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsSetupChangeEvent.java deleted file mode 100644 index e3b26315..00000000 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsSetupChangeEvent.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET) - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl; - -import org.springframework.context.ApplicationEvent; - -import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; - -public class LmsSetupChangeEvent extends ApplicationEvent { - - private static final long serialVersionUID = -7239994198026689531L; - - public LmsSetupChangeEvent(final LmsSetup source) { - super(source); - } - - public LmsSetup getLmsSetup() { - return (LmsSetup) this.source; - } - -} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/SEBRestrictionServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/SEBRestrictionServiceImpl.java index 3b3f6d01..0c2c85a8 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/SEBRestrictionServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/SEBRestrictionServiceImpl.java @@ -18,6 +18,8 @@ import java.util.Map; import java.util.Set; import java.util.stream.Collectors; +import javax.validation.constraints.NotNull; + import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -29,6 +31,7 @@ import ch.ethz.seb.sebserver.gbl.Constants; import ch.ethz.seb.sebserver.gbl.api.EntityType; import ch.ethz.seb.sebserver.gbl.model.exam.Exam; import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction; +import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.Features; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.gbl.util.Result; @@ -62,15 +65,38 @@ public class SEBRestrictionServiceImpl implements SEBRestrictionService { this.examConfigService = examConfigService; } + @Override + public LmsAPIService getLmsAPIService() { + return this.lmsAPIService; + } + + @Override + public boolean checkConsistency(@NotNull final Long lmsSetupId, final Exam exam) { + final LmsSetup lmsSetup = this.lmsAPIService + .getLmsSetup(exam.lmsSetupId) + .getOr(null); + + // check only if SEB_RESTRICTION feature is on + if (lmsSetup != null && lmsSetup.lmsType.features.contains(Features.SEB_RESTRICTION)) { + if (!exam.sebRestriction) { + return false; + } + } + + return true; + } + @Override @Transactional public Result getSEBRestrictionFromExam(final Exam exam) { return Result.tryCatch(() -> { // load the config keys from restriction and merge with new generated config keys + final long currentTimeMillis = System.currentTimeMillis(); final Set configKeys = new HashSet<>(); final Collection generatedKeys = this.examConfigService .generateConfigKeys(exam.institutionId, exam.id) .getOrThrow(); + System.out.println("******* " + (System.currentTimeMillis() - currentTimeMillis)); configKeys.addAll(generatedKeys); if (generatedKeys != null && !generatedKeys.isEmpty()) { @@ -134,6 +160,7 @@ public class SEBRestrictionServiceImpl implements SEBRestrictionService { null, null, null, null, null, null, null, null, null, null, exam.supporter, exam.status, + null, (browserExamKeys != null && !browserExamKeys.isEmpty()) ? StringUtils.join(browserExamKeys, Constants.LIST_SEPARATOR_CHAR) : StringUtils.EMPTY, @@ -167,28 +194,31 @@ public class SEBRestrictionServiceImpl implements SEBRestrictionService { @Override public Result applySEBClientRestriction(final Exam exam) { - if (!this.lmsAPIService - .getLmsSetup(exam.lmsSetupId) - .getOrThrow().lmsType.features.contains(Features.SEB_RESTRICTION)) { + return Result.tryCatch(() -> { + if (!this.lmsAPIService + .getLmsSetup(exam.lmsSetupId) + .getOrThrow().lmsType.features.contains(Features.SEB_RESTRICTION)) { - return Result.of(exam); - } + return exam; + } - return this.getSEBRestrictionFromExam(exam) - .map(sebRestrictionData -> { + return this.getSEBRestrictionFromExam(exam) + .map(sebRestrictionData -> { - if (log.isDebugEnabled()) { - log.debug("Applying SEB Client restriction on LMS with: {}", sebRestrictionData); - } + if (log.isDebugEnabled()) { + log.debug("Applying SEB Client restriction on LMS with: {}", sebRestrictionData); + } - return this.lmsAPIService - .getLmsAPITemplate(exam.lmsSetupId) - .flatMap(lmsTemplate -> lmsTemplate.applySEBClientRestriction( - exam.externalId, - sebRestrictionData)) - .map(data -> exam) - .getOrThrow(); - }); + return this.lmsAPIService + .getLmsAPITemplate(exam.lmsSetupId) + .flatMap(lmsTemplate -> lmsTemplate.applySEBClientRestriction( + exam.externalId, + sebRestrictionData)) + .map(data -> exam) + .getOrThrow(); + }) + .getOrThrow(); + }); } @Override diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxCourseAccess.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxCourseAccess.java index 69af7ac4..54d36cd3 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxCourseAccess.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxCourseAccess.java @@ -10,6 +10,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.edx; import java.net.URL; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -53,12 +54,13 @@ import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Utils; import ch.ethz.seb.sebserver.webservice.WebserviceInfo; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; -import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.CourseAccess; +import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier; +import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.AbstractCourseAccess; /** Implements the LmsAPITemplate for Open edX LMS Course API access. * * See also: https://course-catalog-api-guide.readthedocs.io */ -final class OpenEdxCourseAccess extends CourseAccess { +final class OpenEdxCourseAccess extends AbstractCourseAccess { private static final Logger log = LoggerFactory.getLogger(OpenEdxCourseAccess.class); @@ -70,7 +72,6 @@ final class OpenEdxCourseAccess extends CourseAccess { private static final String OPEN_EDX_DEFAULT_USER_PROFILE_ENDPOINT = "/api/user/v1/accounts?username="; private final JSONMapper jsonMapper; - private final LmsSetup lmsSetup; private final OpenEdxRestTemplateFactory openEdxRestTemplateFactory; private final WebserviceInfo webserviceInfo; private final MemoizingCircuitBreaker> allQuizzesRequest; @@ -80,7 +81,6 @@ final class OpenEdxCourseAccess extends CourseAccess { public OpenEdxCourseAccess( final JSONMapper jsonMapper, - final LmsSetup lmsSetup, final OpenEdxRestTemplateFactory openEdxRestTemplateFactory, final WebserviceInfo webserviceInfo, final AsyncService asyncService, @@ -88,7 +88,6 @@ final class OpenEdxCourseAccess extends CourseAccess { super(asyncService, environment); this.jsonMapper = jsonMapper; - this.lmsSetup = lmsSetup; this.openEdxRestTemplateFactory = openEdxRestTemplateFactory; this.webserviceInfo = webserviceInfo; @@ -130,8 +129,14 @@ final class OpenEdxCourseAccess extends CourseAccess { }; } + APITemplateDataSupplier getApiTemplateDataSupplier() { + return this.openEdxRestTemplateFactory.apiTemplateDataSupplier; + } + LmsSetupTestResult initAPIAccess() { + final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup(); + final LmsSetupTestResult attributesCheck = this.openEdxRestTemplateFactory.test(); if (!attributesCheck.isOk()) { return attributesCheck; @@ -148,13 +153,14 @@ final class OpenEdxCourseAccess extends CourseAccess { final OAuth2RestTemplate restTemplate = restTemplateRequest.get(); try { - this.getEdxPage(this.lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_ENDPOINT, restTemplate); + restTemplate.getAccessToken(); + //this.getEdxPage(lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_ENDPOINT, restTemplate); } catch (final RuntimeException e) { restTemplate.setAuthenticator(new EdxOAuth2RequestAuthenticator()); try { - this.getEdxPage(this.lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_ENDPOINT, restTemplate); + this.getEdxPage(lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_ENDPOINT, restTemplate); } catch (final RuntimeException ee) { log.error("Failed to access Open edX course API: ", ee); return LmsSetupTestResult.ofQuizAccessAPIError(ee.getMessage()); @@ -167,14 +173,18 @@ final class OpenEdxCourseAccess extends CourseAccess { @Override public Result getExamineeAccountDetails(final String examineeSessionId) { return Result.tryCatch(() -> { + final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup(); + final HttpHeaders httpHeaders = new HttpHeaders(); final OAuth2RestTemplate template = getRestTemplate() .getOrThrow(); - final String externalStartURI = this.webserviceInfo.getLmsExternalAddressAlias(this.lmsSetup.lmsApiUrl); + final String externalStartURI = this.webserviceInfo + .getLmsExternalAddressAlias(lmsSetup.lmsApiUrl); + final String uri = (externalStartURI != null) ? externalStartURI + OPEN_EDX_DEFAULT_USER_PROFILE_ENDPOINT + examineeSessionId - : this.lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_USER_PROFILE_ENDPOINT + examineeSessionId; - final HttpHeaders httpHeaders = new HttpHeaders(); + : lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_USER_PROFILE_ENDPOINT + examineeSessionId; + final String responseJSON = template.exchange( uri, HttpMethod.GET, @@ -211,9 +221,24 @@ final class OpenEdxCourseAccess extends CourseAccess { @Override protected Supplier> quizzesSupplier(final Set ids) { - return () -> getRestTemplate() - .map(template -> this.collectQuizzes(template, ids)) - .getOrThrow(); + + if (ids.size() == 1) { + return () -> { + final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup(); + final String externalStartURI = getExternalLMSServerAddress(lmsSetup); + return Arrays.asList(quizDataOf( + lmsSetup, + getOneCourses( + lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_ENDPOINT, + getRestTemplate().getOrThrow(), + ids.iterator().next()), + externalStartURI)); + }; + } else { + return () -> getRestTemplate() + .map(template -> this.collectQuizzes(template, ids)) + .getOrThrow(); + } } private Supplier> quizzesSupplier() { @@ -225,8 +250,10 @@ final class OpenEdxCourseAccess extends CourseAccess { @Override protected Supplier getCourseChaptersSupplier(final String courseId) { return () -> { + final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup(); + final String uri = - this.lmsSetup.lmsApiUrl + + lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_BLOCKS_ENDPOINT + Utils.encodeFormURL_UTF_8(courseId); return new Chapters(getCourseBlocks(uri) @@ -239,16 +266,18 @@ final class OpenEdxCourseAccess extends CourseAccess { } private ArrayList collectQuizzes(final OAuth2RestTemplate restTemplate, final Set ids) { - final String externalStartURI = getExternalLMSServerAddress(this.lmsSetup); + final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup(); + final String externalStartURI = getExternalLMSServerAddress(lmsSetup); + return collectCourses( - this.lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_ENDPOINT, + lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_ENDPOINT, restTemplate, ids) .stream() .reduce( new ArrayList<>(), (list, courseData) -> { - list.add(quizDataOf(this.lmsSetup, courseData, externalStartURI)); + list.add(quizDataOf(lmsSetup, courseData, externalStartURI)); return list; }, (list1, list2) -> { @@ -258,15 +287,16 @@ final class OpenEdxCourseAccess extends CourseAccess { } private ArrayList collectAllQuizzes(final OAuth2RestTemplate restTemplate) { - final String externalStartURI = getExternalLMSServerAddress(this.lmsSetup); + final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup(); + final String externalStartURI = getExternalLMSServerAddress(lmsSetup); return collectAllCourses( - this.lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_ENDPOINT, + lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_ENDPOINT, restTemplate) .stream() .reduce( new ArrayList<>(), (list, courseData) -> { - list.add(quizDataOf(this.lmsSetup, courseData, externalStartURI)); + list.add(quizDataOf(lmsSetup, courseData, externalStartURI)); return list; }, (list1, list2) -> { @@ -322,6 +352,21 @@ final class OpenEdxCourseAccess extends CourseAccess { return collector; } + private CourseData getOneCourses( + final String pageURI, + final OAuth2RestTemplate restTemplate, + final String id) { + + final HttpHeaders httpHeaders = new HttpHeaders(); + final ResponseEntity exchange = restTemplate.exchange( + pageURI + "/" + id, + HttpMethod.GET, + new HttpEntity<>(httpHeaders), + CourseData.class); + + return exchange.getBody(); + } + private List collectAllCourses(final String pageURI, final OAuth2RestTemplate restTemplate) { final List collector = new ArrayList<>(); EdXPage page = getEdxPage(pageURI, restTemplate).getBody(); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxCourseRestriction.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxCourseRestriction.java index 7fc91d32..7176937d 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxCourseRestriction.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxCourseRestriction.java @@ -8,8 +8,6 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.edx; -import java.util.function.BooleanSupplier; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpEntity; @@ -23,8 +21,6 @@ import org.springframework.web.client.HttpClientErrorException; import com.fasterxml.jackson.core.JsonProcessingException; -import ch.ethz.seb.sebserver.gbl.api.APIMessage; -import ch.ethz.seb.sebserver.gbl.api.APIMessage.APIMessageException; import ch.ethz.seb.sebserver.gbl.api.JSONMapper; import ch.ethz.seb.sebserver.gbl.model.exam.OpenEdxSEBRestriction; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; @@ -43,19 +39,16 @@ public class OpenEdxCourseRestriction { private static final String OPEN_EDX_DEFAULT_COURSE_RESTRICTION_API_PATH = "/seb-openedx/api/v1/course/%s/configuration/"; - private final LmsSetup lmsSetup; private final JSONMapper jsonMapper; private final OpenEdxRestTemplateFactory openEdxRestTemplateFactory; private OAuth2RestTemplate restTemplate; protected OpenEdxCourseRestriction( - final LmsSetup lmsSetup, final JSONMapper jsonMapper, final OpenEdxRestTemplateFactory openEdxRestTemplateFactory, final int restrictionAPIPushCount) { - this.lmsSetup = lmsSetup; this.jsonMapper = jsonMapper; this.openEdxRestTemplateFactory = openEdxRestTemplateFactory; } @@ -75,15 +68,16 @@ public class OpenEdxCourseRestriction { } final OAuth2RestTemplate restTemplate = restTemplateRequest.get(); - - // NOTE: since the OPEN_EDX_DEFAULT_COURSE_RESTRICTION_API_INFO endpoint is - // not accessible within OAuth2 authentication (just with user - authentication), - // we can only check if the endpoint is available for now. This is checked - // if there is no 404 response. - // TODO: Ask eduNEXT to implement also OAuth2 API access for this endpoint to be able - // to check the version of the installed plugin. - final String url = this.lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_RESTRICTION_API_INFO; try { + final LmsSetup lmsSetup = this.openEdxRestTemplateFactory.apiTemplateDataSupplier.getLmsSetup(); + + // NOTE: since the OPEN_EDX_DEFAULT_COURSE_RESTRICTION_API_INFO endpoint is + // not accessible within OAuth2 authentication (just with user - authentication), + // we can only check if the endpoint is available for now. This is checked + // if there is no 404 response. + // TODO: Ask eduNEXT to implement also OAuth2 API access for this endpoint to be able + // to check the version of the installed plugin. + final String url = lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_RESTRICTION_API_INFO; restTemplate.exchange( url, @@ -111,8 +105,10 @@ public class OpenEdxCourseRestriction { log.debug("GET SEB Client restriction on course: {}", courseId); } + final LmsSetup lmsSetup = this.openEdxRestTemplateFactory.apiTemplateDataSupplier.getLmsSetup(); + return Result.tryCatch(() -> { - final String url = this.lmsSetup.lmsApiUrl + getSEBRestrictionUrl(courseId); + final String url = lmsSetup.lmsApiUrl + getSEBRestrictionUrl(courseId); final HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); httpHeaders.add(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate"); @@ -146,9 +142,28 @@ public class OpenEdxCourseRestriction { log.debug("PUT SEB Client restriction on course: {} : {}", courseId, restriction); } - return handleSEBRestriction(pushSEBRestrictionFunction( - restriction, - courseId)); + return Result.tryCatch(() -> { + final LmsSetup lmsSetup = this.openEdxRestTemplateFactory.apiTemplateDataSupplier.getLmsSetup(); + final String url = lmsSetup.lmsApiUrl + getSEBRestrictionUrl(courseId); + final HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); + httpHeaders.add(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate"); + final OpenEdxSEBRestriction body = this + .getRestTemplate() + .getOrThrow() + .exchange( + url, + HttpMethod.PUT, + new HttpEntity<>(toJson(restriction), httpHeaders), + OpenEdxSEBRestriction.class) + .getBody(); + + if (log.isDebugEnabled()) { + log.debug("Successfully PUT SEB Client restriction on course: {} : {}", courseId, body); + } + + return true; + }); } Result deleteSEBRestriction(final String courseId) { @@ -157,74 +172,99 @@ public class OpenEdxCourseRestriction { log.debug("DELETE SEB Client restriction on course: {}", courseId); } - return handleSEBRestriction(deleteSEBRestrictionFunction(courseId)); - } - - private BooleanSupplier pushSEBRestrictionFunction( - final OpenEdxSEBRestriction restriction, - final String courseId) { - - final String url = this.lmsSetup.lmsApiUrl + getSEBRestrictionUrl(courseId); - final HttpHeaders httpHeaders = new HttpHeaders(); - httpHeaders.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); - httpHeaders.add(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate"); - return () -> { - final OpenEdxSEBRestriction body = this.restTemplate.exchange( - url, - HttpMethod.PUT, - new HttpEntity<>(toJson(restriction), httpHeaders), - OpenEdxSEBRestriction.class) - .getBody(); - - if (log.isDebugEnabled()) { - log.debug("Successfully PUT SEB Client restriction on course: {} : {}", courseId, body); - } - - return true; - }; - } - - private BooleanSupplier deleteSEBRestrictionFunction(final String courseId) { - - final String url = this.lmsSetup.lmsApiUrl + getSEBRestrictionUrl(courseId); - return () -> { + return Result.tryCatch(() -> { + final LmsSetup lmsSetup = this.openEdxRestTemplateFactory.apiTemplateDataSupplier.getLmsSetup(); + final String url = lmsSetup.lmsApiUrl + getSEBRestrictionUrl(courseId); final HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.add(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate"); - final ResponseEntity exchange = this.restTemplate.exchange( - url, - HttpMethod.DELETE, - new HttpEntity<>(httpHeaders), - Object.class); + final ResponseEntity 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 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 exchange = this.restTemplate.exchange( +// url, +// HttpMethod.DELETE, +// new HttpEntity<>(httpHeaders), +// Object.class); +// +// if (exchange.getStatusCode() == HttpStatus.NO_CONTENT) { +// if (log.isDebugEnabled()) { +// log.debug("Successfully PUT SEB Client restriction on course: {}", courseId); +// } +// } else { +// log.error("Unexpected response for deletion: {}", exchange); +// return false; +// } +// +// return true; +// }; +// } + +// private Result handleSEBRestriction(final BooleanSupplier task) { +// return getRestTemplate() +// .map(restTemplate -> { +// try { +// return task.getAsBoolean(); +// } catch (final HttpClientErrorException ce) { +// if (ce.getStatusCode() == HttpStatus.UNAUTHORIZED) { +// throw new APIMessageException(APIMessage.ErrorMessage.UNAUTHORIZED.of(ce.getMessage() +// + " Unable to get access for API. Please check the corresponding LMS Setup ")); +// } +// throw ce; +// } catch (final Exception e) { +// throw new RuntimeException("Unexpected: ", e); +// } +// }); +// } private String getSEBRestrictionUrl(final String courseId) { return String.format(OPEN_EDX_DEFAULT_COURSE_RESTRICTION_API_PATH, courseId); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxLmsAPITemplate.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxLmsAPITemplate.java index 918a0022..47993903 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxLmsAPITemplate.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxLmsAPITemplate.java @@ -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); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxLmsAPITemplateFactory.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxLmsAPITemplateFactory.java index bcb7980e..1ad00b7b 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxLmsAPITemplateFactory.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxLmsAPITemplateFactory.java @@ -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 create( - final LmsSetup lmsSetup, - final ClientCredentials credentials, - final ProxyData proxyData) { + public Result 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); }); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxRestTemplateFactory.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxRestTemplateFactory.java index f1454ad4..7959a66b 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxRestTemplateFactory.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxRestTemplateFactory.java @@ -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 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 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 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); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/MockLmsAPITemplateFactory.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockLmsAPITemplateFactory.java similarity index 71% rename from src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/MockLmsAPITemplateFactory.java rename to src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockLmsAPITemplateFactory.java index 6922ad13..2cf628c5 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/MockLmsAPITemplateFactory.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockLmsAPITemplateFactory.java @@ -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 create( - final LmsSetup lmsSetup, - final ClientCredentials credentials, - final ProxyData proxyData) { - + public Result create(final APITemplateDataSupplier apiTemplateDataSupplier) { return Result.tryCatch(() -> new MockupLmsAPITemplate( - lmsSetup, - credentials, + apiTemplateDataSupplier, this.webserviceInfo)); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/MockupLmsAPITemplate.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockupLmsAPITemplate.java similarity index 88% rename from src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/MockupLmsAPITemplate.java rename to src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockupLmsAPITemplate.java index fc9342e5..cb1d22c9 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/MockupLmsAPITemplate.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockupLmsAPITemplate.java @@ -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 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)", "

Demo Quiz Mockup

", "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 checkAttributes() { + final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup(); + final ClientCredentials lmsClientCredentials = this.apiTemplateDataSupplier.getLmsClientCredentials(); final List 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 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"); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleCourseAccess.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleCourseAccess.java index 6255c5d9..7d898892 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleCourseAccess.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleCourseAccess.java @@ -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> 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 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 getCached() { final Collection 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 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 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()); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleLmsAPITemplate.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleLmsAPITemplate.java index 3cdb489f..18560d03 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleLmsAPITemplate.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleLmsAPITemplate.java @@ -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 diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleLmsAPITemplateFactory.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleLmsAPITemplateFactory.java index 190fb21e..7659d3a7 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleLmsAPITemplateFactory.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleLmsAPITemplateFactory.java @@ -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 create( - final LmsSetup lmsSetup, - final ClientCredentials credentials, - final ProxyData proxyData) { + public Result 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); }); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleRestTemplateFactory.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleRestTemplateFactory.java index 1db8ded9..682e3140 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleRestTemplateFactory.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleRestTemplateFactory.java @@ -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 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 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 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 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 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()); } } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java index b90b5b7b..c65af50f 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java @@ -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 diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java index 0db2eda3..9da295d5 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java @@ -246,12 +246,21 @@ public class ExamAdministrationController extends EntityController { @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 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 { } private Result 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, List> pageSort(final String sort) { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/LmsSetupController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/LmsSetupController.java index 51934319..5ec15070 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/LmsSetupController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/LmsSetupController.java @@ -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 notifySaved(final LmsSetup entity) { - this.applicationEventPublisher.publishEvent(new LmsSetupChangeEvent(entity)); - return super.notifySaved(entity); - } - } diff --git a/src/test/java/ch/ethz/seb/sebserver/gbl/model/ModelObjectJSONGenerator.java b/src/test/java/ch/ethz/seb/sebserver/gbl/model/ModelObjectJSONGenerator.java index ac37b726..2d2113d2 100644 --- a/src/test/java/ch/ethz/seb/sebserver/gbl/model/ModelObjectJSONGenerator.java +++ b/src/test/java/ch/ethz/seb/sebserver/gbl/model/ModelObjectJSONGenerator.java @@ -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)); diff --git a/src/test/java/ch/ethz/seb/sebserver/gui/integration/UseCasesIntegrationTest.java b/src/test/java/ch/ethz/seb/sebserver/gui/integration/UseCasesIntegrationTest.java index ed98c574..f3dc9949 100644 --- a/src/test/java/ch/ethz/seb/sebserver/gui/integration/UseCasesIntegrationTest.java +++ b/src/test/java/ch/ethz/seb/sebserver/gui/integration/UseCasesIntegrationTest.java @@ -850,6 +850,7 @@ public class UseCasesIntegrationTest extends GuiIntegrationTest { null, Utils.immutableCollectionOf(userId), ExamStatus.RUNNING, + false, null, true, null); diff --git a/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/admin/ExamAPITest.java b/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/admin/ExamAPITest.java index 0c84221c..9ed74080 100644 --- a/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/admin/ExamAPITest.java +++ b/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/admin/ExamAPITest.java @@ -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)) diff --git a/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/admin/ExamImportTest.java b/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/admin/ExamImportTest.java index bbbcf485..8f8178f3 100644 --- a/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/admin/ExamImportTest.java +++ b/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/admin/ExamImportTest.java @@ -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"); diff --git a/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleCourseAccessTest.java b/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleCourseAccessTest.java index ec1da20a..d29e5946 100644 --- a/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleCourseAccessTest.java +++ b/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleCourseAccessTest.java @@ -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),