refactoring and improvement of LMS binding API
This commit is contained in:
		
							parent
							
								
									0bda2cb292
								
							
						
					
					
						commit
						dd6150ec2a
					
				
					 36 changed files with 790 additions and 544 deletions
				
			
		|  | @ -129,6 +129,7 @@ public final class API { | ||||||
|     public static final String EXAM_ADMINISTRATION_ENDPOINT = "/exam"; |     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_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_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_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_RESTRICTION_PATH_SEGMENT = "/check-seb-restriction"; | ||||||
|     public static final String EXAM_ADMINISTRATION_CHECK_IMPORTED_PATH_SEGMENT = "/check-imported"; |     public static final String EXAM_ADMINISTRATION_CHECK_IMPORTED_PATH_SEGMENT = "/check-imported"; | ||||||
|  |  | ||||||
|  | @ -47,7 +47,7 @@ public final class Exam implements GrantEntity { | ||||||
|             null, |             null, | ||||||
|             null, |             null, | ||||||
|             ExamStatus.FINISHED, |             ExamStatus.FINISHED, | ||||||
| //            Boolean.FALSE, |             Boolean.FALSE, | ||||||
|             null, |             null, | ||||||
|             Boolean.FALSE, |             Boolean.FALSE, | ||||||
|             null); |             null); | ||||||
|  | @ -115,6 +115,9 @@ public final class Exam implements GrantEntity { | ||||||
|     @JsonProperty(EXAM.ATTR_STATUS) |     @JsonProperty(EXAM.ATTR_STATUS) | ||||||
|     public final ExamStatus status; |     public final ExamStatus status; | ||||||
| 
 | 
 | ||||||
|  |     @JsonProperty(EXAM.ATTR_LMS_SEB_RESTRICTION) | ||||||
|  |     public final Boolean sebRestriction; | ||||||
|  | 
 | ||||||
|     @JsonProperty(EXAM.ATTR_BROWSER_KEYS) |     @JsonProperty(EXAM.ATTR_BROWSER_KEYS) | ||||||
|     public final String browserExamKeys; |     public final String browserExamKeys; | ||||||
| 
 | 
 | ||||||
|  | @ -139,6 +142,7 @@ public final class Exam implements GrantEntity { | ||||||
|             @JsonProperty(EXAM.ATTR_OWNER) final String owner, |             @JsonProperty(EXAM.ATTR_OWNER) final String owner, | ||||||
|             @JsonProperty(EXAM.ATTR_SUPPORTER) final Collection<String> supporter, |             @JsonProperty(EXAM.ATTR_SUPPORTER) final Collection<String> supporter, | ||||||
|             @JsonProperty(EXAM.ATTR_STATUS) final ExamStatus status, |             @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_BROWSER_KEYS) final String browserExamKeys, | ||||||
|             @JsonProperty(EXAM.ATTR_ACTIVE) final Boolean active, |             @JsonProperty(EXAM.ATTR_ACTIVE) final Boolean active, | ||||||
|             @JsonProperty(EXAM.ATTR_LASTUPDATE) final String lastUpdate) { |             @JsonProperty(EXAM.ATTR_LASTUPDATE) final String lastUpdate) { | ||||||
|  | @ -155,6 +159,7 @@ public final class Exam implements GrantEntity { | ||||||
|         this.type = type; |         this.type = type; | ||||||
|         this.owner = owner; |         this.owner = owner; | ||||||
|         this.status = (status != null) ? status : getStatusFromDate(startTime, endTime); |         this.status = (status != null) ? status : getStatusFromDate(startTime, endTime); | ||||||
|  |         this.sebRestriction = sebRestriction; | ||||||
|         this.browserExamKeys = browserExamKeys; |         this.browserExamKeys = browserExamKeys; | ||||||
|         this.active = (active != null) ? active : Boolean.TRUE; |         this.active = (active != null) ? active : Boolean.TRUE; | ||||||
|         this.lastUpdate = lastUpdate; |         this.lastUpdate = lastUpdate; | ||||||
|  | @ -181,6 +186,7 @@ public final class Exam implements GrantEntity { | ||||||
|                 EXAM.ATTR_STATUS, |                 EXAM.ATTR_STATUS, | ||||||
|                 ExamStatus.class, |                 ExamStatus.class, | ||||||
|                 getStatusFromDate(this.startTime, this.endTime)); |                 getStatusFromDate(this.startTime, this.endTime)); | ||||||
|  |         this.sebRestriction = null; | ||||||
|         this.browserExamKeys = mapper.getString(EXAM.ATTR_BROWSER_KEYS); |         this.browserExamKeys = mapper.getString(EXAM.ATTR_BROWSER_KEYS); | ||||||
|         this.active = mapper.getBoolean(EXAM.ATTR_ACTIVE); |         this.active = mapper.getBoolean(EXAM.ATTR_ACTIVE); | ||||||
|         this.supporter = mapper.getStringSet(EXAM.ATTR_SUPPORTER); |         this.supporter = mapper.getStringSet(EXAM.ATTR_SUPPORTER); | ||||||
|  | @ -204,6 +210,7 @@ public final class Exam implements GrantEntity { | ||||||
|         this.type = null; |         this.type = null; | ||||||
|         this.owner = null; |         this.owner = null; | ||||||
|         this.status = (status != null) ? status : getStatusFromDate(this.startTime, this.endTime); |         this.status = (status != null) ? status : getStatusFromDate(this.startTime, this.endTime); | ||||||
|  |         this.sebRestriction = null; | ||||||
|         this.browserExamKeys = null; |         this.browserExamKeys = null; | ||||||
|         this.active = null; |         this.active = null; | ||||||
|         this.supporter = null; |         this.supporter = null; | ||||||
|  |  | ||||||
|  | @ -29,6 +29,7 @@ import ch.ethz.seb.sebserver.gbl.util.Utils; | ||||||
| 
 | 
 | ||||||
| public final class QuizData implements GrantEntity { | 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 FILTER_ATTR_START_TIME = "start_timestamp"; | ||||||
| 
 | 
 | ||||||
|     public static final String ATTR_ADDITIONAL_ATTRIBUTES = "ADDITIONAL_ATTRIBUTES"; |     public static final String ATTR_ADDITIONAL_ATTRIBUTES = "ADDITIONAL_ATTRIBUTES"; | ||||||
|  |  | ||||||
|  | @ -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_SETUP = "lms_setup"; | ||||||
|     public static final String FILTER_ATTR_LMS_TYPE = "lms_type"; |     public static final String FILTER_ATTR_LMS_TYPE = "lms_type"; | ||||||
| 
 | 
 | ||||||
|  |     /** LMS binding and API features */ | ||||||
|     public enum 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, |         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 |         SEB_RESTRICTION | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** Defines the supported types if LMS bindings. | ||||||
|  |      * Also defines the supports feature(s) for each type of LMS binding. */ | ||||||
|     public enum LmsType { |     public enum LmsType { | ||||||
|  |         /** Mockup LMS type used to create test setups */ | ||||||
|         MOCKUP(Features.COURSE_API), |         MOCKUP(Features.COURSE_API), | ||||||
|  |         /** The Open edX LMS binding features both APIs, course access as well as SEB restrcition */ | ||||||
|         OPEN_EDX(Features.COURSE_API, Features.SEB_RESTRICTION), |         OPEN_EDX(Features.COURSE_API, Features.SEB_RESTRICTION), | ||||||
|  |         /** The Moodle binding features only the course access API so far */ | ||||||
|         MOODLE(Features.COURSE_API /* , Features.SEB_RESTRICTION */), |         MOODLE(Features.COURSE_API /* , Features.SEB_RESTRICTION */), | ||||||
|  |         /** The Ans Delft binding is on the way */ | ||||||
|         ANS_DELFT(/* Features.COURSE_API , Features.SEB_RESTRICTION */), |         ANS_DELFT(/* Features.COURSE_API , Features.SEB_RESTRICTION */), | ||||||
|  |         /** The OpenOLAT binding is on the way */ | ||||||
|         OPEN_OLAT(/* Features.COURSE_API , Features.SEB_RESTRICTION */); |         OPEN_OLAT(/* Features.COURSE_API , Features.SEB_RESTRICTION */); | ||||||
| 
 | 
 | ||||||
|         public final EnumSet<Features> features; |         public final EnumSet<Features> features; | ||||||
|  |  | ||||||
|  | @ -226,6 +226,8 @@ public class ExamForm implements TemplateComposer { | ||||||
|         final ExamStatus examStatus = exam.getStatus(); |         final ExamStatus examStatus = exam.getStatus(); | ||||||
|         final boolean editable = modifyGrant && (examStatus == ExamStatus.UP_COMING || |         final boolean editable = modifyGrant && (examStatus == ExamStatus.UP_COMING || | ||||||
|                 examStatus == ExamStatus.RUNNING); |                 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 sebRestrictionAvailable = testSEBRestrictionAPI(exam); | ||||||
|         final boolean isRestricted = readonly && sebRestrictionAvailable && this.restService |         final boolean isRestricted = readonly && sebRestrictionAvailable && this.restService | ||||||
|                 .getBuilder(CheckSEBRestriction.class) |                 .getBuilder(CheckSEBRestriction.class) | ||||||
|  |  | ||||||
|  | @ -29,4 +29,5 @@ public interface LmsSetupDAO extends ActivatableEntityDAO<LmsSetup, LmsSetup>, B | ||||||
|      * @param lmsSetupId the LMS Setup identifier |      * @param lmsSetupId the LMS Setup identifier | ||||||
|      * @return Result refer to the proxy data or to an error if happened */ |      * @return Result refer to the proxy data or to an error if happened */ | ||||||
|     Result<ProxyData> getLmsAPIAccessProxyData(String lmsSetupId); |     Result<ProxyData> getLmsAPIAccessProxyData(String lmsSetupId); | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1010,6 +1010,7 @@ public class ExamDAOImpl implements ExamDAO { | ||||||
|                     record.getOwner(), |                     record.getOwner(), | ||||||
|                     supporter, |                     supporter, | ||||||
|                     (quizData != null) ? status : (statusOverride != null) ? statusOverride : status, |                     (quizData != null) ? status : (statusOverride != null) ? statusOverride : status, | ||||||
|  |                     BooleanUtils.toBooleanObject(record.getLmsSebRestriction()), | ||||||
|                     record.getBrowserKeys(), |                     record.getBrowserKeys(), | ||||||
|                     BooleanUtils.toBooleanObject((quizData != null) ? record.getActive() : null), |                     BooleanUtils.toBooleanObject((quizData != null) ? record.getActive() : null), | ||||||
|                     record.getLastupdate()); |                     record.getLastupdate()); | ||||||
|  |  | ||||||
|  | @ -41,17 +41,6 @@ public interface ExamAdminService { | ||||||
|      * @return Result refer to the restriction flag or to an error when happened */ |      * @return Result refer to the restriction flag or to an error when happened */ | ||||||
|     Result<Boolean> isRestricted(Exam exam); |     Result<Boolean> isRestricted(Exam exam); | ||||||
| 
 | 
 | ||||||
| //    /** Get the proctoring service settings for a certain exam to an error when happened. |  | ||||||
| //     * |  | ||||||
| //     * @param examId the exam instance |  | ||||||
| //     * @return Result refer to proctoring service settings for the exam. */ |  | ||||||
| //    default Result<ProctoringServiceSettings> getProctoringServiceSettings(final Exam exam) { |  | ||||||
| //        if (exam == null || exam.id == null) { |  | ||||||
| //            return Result.ofRuntimeError("Invalid Exam model"); |  | ||||||
| //        } |  | ||||||
| //        return getProctoringServiceSettings(exam.id); |  | ||||||
| //    } |  | ||||||
| 
 |  | ||||||
|     /** Get proctoring service settings for a certain exam to an error when happened. |     /** Get proctoring service settings for a certain exam to an error when happened. | ||||||
|      * |      * | ||||||
|      * @param examId the exam identifier |      * @param examId the exam identifier | ||||||
|  | @ -99,29 +88,4 @@ public interface ExamAdminService { | ||||||
|                 .flatMap(settings -> getExamProctoringService(settings.serverType)); |                 .flatMap(settings -> getExamProctoringService(settings.serverType)); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| //    /** Get the exam proctoring service implementation of specified type. |  | ||||||
| //     * |  | ||||||
| //     * @param settings the ProctoringSettings that defines the ProctoringServerType |  | ||||||
| //     * @return ExamProctoringService instance */ |  | ||||||
| //    default Result<ExamProctoringService> getExamProctoringService(final ProctoringServiceSettings settings) { |  | ||||||
| //        return Result.tryCatch(() -> getExamProctoringService(settings.serverType).getOrThrow()); |  | ||||||
| //    } |  | ||||||
| // |  | ||||||
| //    /** Get the exam proctoring service implementation for specified exam. |  | ||||||
| //     * |  | ||||||
| //     * @param exam the exam instance |  | ||||||
| //     * @return ExamProctoringService instance */ |  | ||||||
| //    default Result<ExamProctoringService> getExamProctoringService(final Exam exam) { |  | ||||||
| //        return Result.tryCatch(() -> getExamProctoringService(exam.id).getOrThrow()); |  | ||||||
| //    } |  | ||||||
| // |  | ||||||
| //    /** Get the exam proctoring service implementation for specified exam. |  | ||||||
| //     * |  | ||||||
| //     * @param examId the exam identifier |  | ||||||
| //     * @return ExamProctoringService instance */ |  | ||||||
| //    default Result<ExamProctoringService> getExamProctoringService(final Long examId) { |  | ||||||
| //        return getProctoringServiceSettings(examId) |  | ||||||
| //                .flatMap(this::getExamProctoringService); |  | ||||||
| //    } |  | ||||||
| 
 |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -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(); | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -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. */ |  * changes this service will be notifies about the change and release the related LmsAPITemplate from cache. */ | ||||||
| public interface LmsAPIService { | public interface LmsAPIService { | ||||||
| 
 | 
 | ||||||
|  |     /** Reset and cleanup the caches if there are some */ | ||||||
|  |     void cleanup(); | ||||||
|  | 
 | ||||||
|     /** Get the specified LmsSetup model by primary key |     /** Get the specified LmsSetup model by primary key | ||||||
|      * |      * | ||||||
|      * @param id The identifier (PK) of the LmsSetup model |      * @param id The identifier (PK) of the LmsSetup model | ||||||
|  |  | ||||||
|  | @ -17,6 +17,7 @@ import java.util.Set; | ||||||
| import org.apache.commons.lang3.StringUtils; | import org.apache.commons.lang3.StringUtils; | ||||||
| 
 | 
 | ||||||
| import ch.ethz.seb.sebserver.gbl.api.EntityType; | 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.Chapters; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.exam.Exam; | import ch.ethz.seb.sebserver.gbl.model.exam.Exam; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.exam.QuizData; | import ch.ethz.seb.sebserver.gbl.model.exam.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.gbl.util.Result; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; | import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ResourceNotFoundException; | 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. | /** Defines an LMS API access template to build SEB Server LMS integration. | ||||||
|  * |  * </p> | ||||||
|  * A LMS integration consists of two main parts so far: |  * 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 |  * </p> | ||||||
|  * 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 |  * <pre> | ||||||
|  * a specific LMS instance of implemented type. |  * - The course API to search and request course data from LMS as well as resolve some | ||||||
|  * |  *   LMS account details for a given examineeId. | ||||||
|  * The enum LmsSetup.LmsType defines the supported LMS types and for each type the supported API part(s). |  * - The SEB restriction API to apply SEB restriction data to the LMS to restrict a | ||||||
|  * |  *   certain course for SEB. | ||||||
|  * SEB Server uses the test functions that are defined for each LMS API part to test API access for a certain LMS |  * </pre> | ||||||
|  * instance respectively the underling LMSSetup. Concrete implementations can do various tests to check full |  * </p> | ||||||
|  * or partial API Access and can flag missing or wrong LMSSetup attributes with the resulting LmsSetupTestResult. |  | ||||||
|  * |  * | ||||||
|  |  * <b>Course API</b></br> | ||||||
|  |  * All course API requests of this template shall not block and return as fast as possible | ||||||
|  |  * with the best result it can provide for the time on that the request was made. | ||||||
|  |  * </p> | ||||||
|  |  * Since the course API requests course data from potentially thousands of existing and | ||||||
|  |  * active courses, the course API can implement some caches if needed.</br> | ||||||
|  |  * A cache in the course API has manly two purposes; The first and prior purpose is to | ||||||
|  |  * be able to provide course data as fast as possible even if the LMS is not available or | ||||||
|  |  * busy at the time. The second purpose is to guarantee fast data access for the system | ||||||
|  |  * if this is needed and data actuality has second priority.</br> | ||||||
|  |  * Therefore usual get quiz data functions like {@link #getQuizzes(FilterMap filterMap) }, | ||||||
|  |  * {@link #getQuizzes(Set<String> ids) } and {@link #getQuiz(final String id) } | ||||||
|  |  * shall always first try to connect to the LMS and request the specified data from the LMS. | ||||||
|  |  * If this succeeds the cache shall be updated with the received quizzes data and return them. | ||||||
|  |  * If this is not possible within a certain time, the implementation shall get as much of the | ||||||
|  |  * requested data from the cache and return them to the caller to not block the call too long | ||||||
|  |  * and allow the caller to return fast and present as much data as possible.</br> | ||||||
|  |  * This can be done with a {@link MemoizingCircuitBreaker} or a simple {@link CircuitBreaker} | ||||||
|  |  * with a separated cache, for example. The abstract implementation; {@link AbstractCourseAccess} | ||||||
|  |  * provides already defined wrapped circuit breaker for each call. To use it, just extend the | ||||||
|  |  * abstract class and implement the needed suppliers.</br> | ||||||
|  |  * On the other hand, dedicated cache access functions like {@link #getQuizzesFromCache(Set<String> ids) } | ||||||
|  |  * shall always first look into the cache to geht the requested data and if not | ||||||
|  |  * available, call the LMS and request the data from the LMS. If partial data is needed to get | ||||||
|  |  * be requested from the LMS, this functions shall also update the catch with the requested | ||||||
|  |  * and cache missed data afterwards. | ||||||
|  |  * </p> | ||||||
|  |  * <b>SEB restriction API</b></br> | ||||||
|  |  * For this API we need no caching since this is mostly about pushing data to the LMS for the LMS | ||||||
|  |  * to use. But this calls sahl also be protected within some kind of circuit breaker pattern to | ||||||
|  |  * avoid blocking on long latency. | ||||||
|  |  * </p> | ||||||
|  |  * </p> | ||||||
|  |  * A {@link LmsAPITemplate } will be constructed within the application with a {@link LmsSetup } instances. | ||||||
|  |  * The application constructs a {@link LmsAPITemplate } for each type of LMS setup when needed or requested and | ||||||
|  |  * there is not already a cached template or the cached template is out of date.</br> | ||||||
|  |  * The {@link LmsSetup } defines the data that is needed to connect to a specific LMS instance of implemented type | ||||||
|  |  * and is wrapped within a {@link LmsAPITemplate } instance that lives as long as there are no changes to the | ||||||
|  |  * {@link LmsSetup and the {@link LmsSetup } that is wrapped within the {@link LmsAPITemplate } is up to date. | ||||||
|  |  * <p> | ||||||
|  |  * The enum {@link LmsSetup.LmsType } defines the supported LMS types and for each type the supported API part(s). | ||||||
|  |  * <p> | ||||||
|  |  * The application uses the test functions that are defined for each LMS API part to test API access for a certain LMS | ||||||
|  |  * instance respectively the underling {@link LmsSetup }. Concrete implementations can do various tests to check full | ||||||
|  |  * or partial API Access and can flag missing or wrong {@link LmsSetup } attributes with the resulting | ||||||
|  |  * {@link LmsSetupTestResult }.</br> | ||||||
|  * SEB Server than uses an instance of this template to communicate with the an LMS. */ |  * SEB Server than uses an instance of this template to communicate with the an LMS. */ | ||||||
| public interface LmsAPITemplate { | 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(); |     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, |      * LMS and the course API of the LMS can be accessed or if there are some difficulties, | ||||||
|      * missing configuration data or connection/authentication errors. |      * 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(); |     LmsSetupTestResult testCourseAccessAPI(); | ||||||
| 
 | 
 | ||||||
|     /** Performs a test for the underling LmsSetup configuration and checks if the |     /** Get an unsorted List of filtered {@link QuizData } from the LMS course/quiz API | ||||||
|      * 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 LmsSetupTestResult instance with the test result report */ |      * @param filterMap the {@link FilterMap } to get a filtered result. Possible filter attributes are: | ||||||
|     LmsSetupTestResult testCourseRestrictionAPI(); |  | ||||||
| 
 |  | ||||||
|     /** Get an unsorted List of filtered QuizData from the LMS course/quiz API |  | ||||||
|      * |      * | ||||||
|      * @param filterMap the FilterMap to get a filtered result. For possible filter attributes |      *            <pre> | ||||||
|      *            see documentation on QuizData |      *      {@link QuizData.FILTER_ATTR_QUIZ_NAME } The quiz name filter text (exclude all names that do not contain the given text) | ||||||
|      * @return Result of an unsorted List of filtered QuizData from the LMS course/quiz API |      *      {@link QuizData.FILTER_ATTR_START_TIME } The quiz start time (exclude all quizzes that starts before) | ||||||
|  |      *            </pre> | ||||||
|  |      * | ||||||
|  |      * @return Result of an unsorted List of filtered {@link QuizData } from the LMS course/quiz API | ||||||
|      *         or refer to an error when happened */ |      *         or refer to an error when happened */ | ||||||
|     Result<List<QuizData>> getQuizzes(FilterMap filterMap); |     Result<List<QuizData>> getQuizzes(FilterMap filterMap); | ||||||
| 
 | 
 | ||||||
|     /** Get all QuizData for the set of QuizData identifiers from LMS API in a collection |     /** 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, |      * of Result. If particular quiz cannot be loaded because of errors or deletion, | ||||||
|      * the Result will have an error reference. |      * the Result will have an error reference. | ||||||
|      * |      * | ||||||
|      * @param ids the Set of Quiz identifiers to get the QuizData for |      * @param ids the Set of Quiz identifiers to get the {@link QuizData } for | ||||||
|      * @return Collection of all QuizData from the given id set */ |      * @return Collection of all {@link QuizData } from the given id set */ | ||||||
|     Collection<Result<QuizData>> getQuizzes(Set<String> ids); |     Collection<Result<QuizData>> getQuizzes(Set<String> ids); | ||||||
| 
 | 
 | ||||||
|     /** Get the quiz data with specified identifier. |     /** Get the quiz data with specified identifier. | ||||||
|      * |      * | ||||||
|      * Default implementation: Uses getQuizzes(Set<String> ids) and returns the first matching or an error. |      * Default implementation: Uses {@link #getQuizzes(Set<String> ids) } and returns the first matching or an error. | ||||||
|      * |      * | ||||||
|      * @param id the quiz data identifier |      * @param id the quiz data identifier | ||||||
|      * @return Result refer to the quiz data or to an error when happened */ |      * @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))); |                 .orElse(Result.ofError(new ResourceNotFoundException(EntityType.EXAM, id))); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** Get all QuizData for the set of QuizData-identifiers (ids) from the LMS defined within the |     /** 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. |      * underling LmsSetup, in a collection of Results. | ||||||
|      * |      * | ||||||
|      * If there is caching involved this function shall try to get the data from the cache first. |      * 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 |      * NOTE: This function depends on the specific LMS implementation and on whether caching the quiz data | ||||||
|      * makes sense or not. Following strategy is recommended: |      * 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 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 |      * 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 |      * @param ids the Set of Quiz identifiers to get the {@link QuizData } for | ||||||
|      * @return Collection of all QuizData from the given id set */ |      * @return Collection of all {@link QuizData } from the given id set */ | ||||||
|     Collection<Result<QuizData>> getQuizzesFromCache(Set<String> ids); |     Collection<Result<QuizData>> getQuizzesFromCache(Set<String> ids); | ||||||
| 
 | 
 | ||||||
|     /** Convert an anonymous or temporary examineeUserId, sent by the SEB Client on LMS login, |     /** 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 |      * 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 |      * @param examineeUserId the examinee user identifier derived from SEB Client | ||||||
|      * @return a Result refer to the ExamineeAccountDetails instance or to an error when happened or not supported */ |      * @return a Result refer to the {@link ExamineeAccountDetails } instance or to an error when happened or not | ||||||
|  |      *         supported */ | ||||||
|     Result<ExamineeAccountDetails> getExamineeAccountDetails(String examineeUserId); |     Result<ExamineeAccountDetails> getExamineeAccountDetails(String examineeUserId); | ||||||
| 
 | 
 | ||||||
|     /** Used to convert an anonymous or temporary examineeUserId, sent by the SEB Client on LMS login, |     /** 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. */ |      * @return Result referencing to the Chapters model for the given course or to an error when happened. */ | ||||||
|     Result<Chapters> getCourseChapters(String courseId); |     Result<Chapters> getCourseChapters(String courseId); | ||||||
| 
 | 
 | ||||||
|     /** Get SEB restriction data form LMS within a SEBRestrictionData instance. The available restriction details |     // **************************************************************************** | ||||||
|  |     // **** SEB restriction API functions ***************************************** | ||||||
|  | 
 | ||||||
|  |     /** Performs a test for the underling {@link LmsSetup } configuration and checks if the | ||||||
|  |      * LMS and the course restriction API of the LMS can be accessed or if there are some difficulties, | ||||||
|  |      * missing configuration data or connection/authentication errors. | ||||||
|  |      * | ||||||
|  |      * @return {@link LmsSetupTestResult } instance with the test result report */ | ||||||
|  |     LmsSetupTestResult testCourseRestrictionAPI(); | ||||||
|  | 
 | ||||||
|  |     /** Get SEB restriction data form LMS within a {@link SEBRestrictionData } instance. The available restriction | ||||||
|  |      * details | ||||||
|      * depends on the type of LMS but shall at least contains the config-key(s) and the browser-exam-key(s). |      * 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 |      * @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 */ |      *         missing or to another exception on unexpected error case */ | ||||||
|     Result<SEBRestriction> getSEBClientRestriction(Exam exam); |     Result<SEBRestriction> getSEBClientRestriction(Exam exam); | ||||||
| 
 | 
 | ||||||
|  | @ -154,7 +218,8 @@ public interface LmsAPITemplate { | ||||||
|      * |      * | ||||||
|      * @param externalExamId The exam/course identifier from LMS side (Exam.externalId) |      * @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 |      * @param sebRestrictionData containing all data for SEB Client restriction to apply to the LMS | ||||||
|      * @return Result refer to the given SEBRestrictionData if restriction was successful or to an error if not */ |      * @return Result refer to the given {@link SEBRestrictionData } if restriction was successful or to an error if | ||||||
|  |      *         not */ | ||||||
|     Result<SEBRestriction> applySEBClientRestriction( |     Result<SEBRestriction> applySEBClientRestriction( | ||||||
|             String externalExamId, |             String externalExamId, | ||||||
|             SEBRestriction sebRestrictionData); |             SEBRestriction sebRestrictionData); | ||||||
|  |  | ||||||
|  | @ -8,9 +8,6 @@ | ||||||
| 
 | 
 | ||||||
| package ch.ethz.seb.sebserver.webservice.servicelayer.lms; | 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.model.institution.LmsSetup.LmsType; | ||||||
| import ch.ethz.seb.sebserver.gbl.util.Result; | import ch.ethz.seb.sebserver.gbl.util.Result; | ||||||
| 
 | 
 | ||||||
|  | @ -23,16 +20,10 @@ public interface LmsAPITemplateFactory { | ||||||
|      * @return the LMS type if a specific implementation */ |      * @return the LMS type if a specific implementation */ | ||||||
|     LmsType lmsType(); |     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 apiTemplateDataSupplier supplies all needed actual LMS setup data */ | ||||||
|      * @param credentials the access data for accessing the LMS API. Either client credentials or access token from LMS |     Result<LmsAPITemplate> create(final APITemplateDataSupplier apiTemplateDataSupplier); | ||||||
|      *            setup input |  | ||||||
|      * @param proxyData The proxy data used to connect to the LMS if needed. |  | ||||||
|      * @return Result refer to the LmsAPITemplate or to an error when happened */ |  | ||||||
|     Result<LmsAPITemplate> create( |  | ||||||
|             final LmsSetup lmsSetup, |  | ||||||
|             final ClientCredentials credentials, |  | ||||||
|             final ProxyData proxyData); |  | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -8,6 +8,8 @@ | ||||||
| 
 | 
 | ||||||
| package ch.ethz.seb.sebserver.webservice.servicelayer.lms; | 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.Exam; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction; | import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction; | ||||||
| import ch.ethz.seb.sebserver.gbl.util.Result; | import ch.ethz.seb.sebserver.gbl.util.Result; | ||||||
|  | @ -19,6 +21,9 @@ public interface SEBRestrictionService { | ||||||
| 
 | 
 | ||||||
|     String SEB_RESTRICTION_ADDITIONAL_PROPERTY_CONFIG_KEY = "config_key"; |     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. |     /** Get the SEBRestriction properties for specified Exam. | ||||||
|      * |      * | ||||||
|      * @param exam the 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 */ |      * @return Result refer to the Exam instance or to an error if happened */ | ||||||
|     Result<Exam> releaseSEBClientRestriction(Exam exam); |     Result<Exam> releaseSEBClientRestriction(Exam exam); | ||||||
| 
 | 
 | ||||||
|  |     boolean checkConsistency(@NotNull Long lmsSetupId, Exam exam); | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -35,9 +35,9 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; | ||||||
|  * API requests in a protected environment. |  * API requests in a protected environment. | ||||||
|  * |  * | ||||||
|  * Extend this to implement a concrete course access API for a given type of LMS. */ |  * 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 |     /** Fetch status that indicates an asynchronous quiz data fetch status if the | ||||||
|      * concrete implementation has such. */ |      * concrete implementation has such. */ | ||||||
|  | @ -54,7 +54,7 @@ public abstract class CourseAccess { | ||||||
|     /** CircuitBreaker for protected examinee account details requests */ |     /** CircuitBreaker for protected examinee account details requests */ | ||||||
|     protected final CircuitBreaker<ExamineeAccountDetails> accountDetailRequest; |     protected final CircuitBreaker<ExamineeAccountDetails> accountDetailRequest; | ||||||
| 
 | 
 | ||||||
|     protected CourseAccess( |     protected AbstractCourseAccess( | ||||||
|             final AsyncService asyncService, |             final AsyncService asyncService, | ||||||
|             final Environment environment) { |             final Environment environment) { | ||||||
| 
 | 
 | ||||||
|  | @ -22,10 +22,10 @@ import org.apache.commons.lang3.StringUtils; | ||||||
| import org.slf4j.Logger; | import org.slf4j.Logger; | ||||||
| import org.slf4j.LoggerFactory; | import org.slf4j.LoggerFactory; | ||||||
| import org.springframework.context.annotation.Lazy; | import org.springframework.context.annotation.Lazy; | ||||||
| import org.springframework.context.event.EventListener; |  | ||||||
| import org.springframework.stereotype.Service; | import org.springframework.stereotype.Service; | ||||||
| 
 | 
 | ||||||
| import ch.ethz.seb.sebserver.gbl.Constants; | 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.ClientCredentialService; | ||||||
| import ch.ethz.seb.sebserver.gbl.client.ClientCredentials; | import ch.ethz.seb.sebserver.gbl.client.ClientCredentials; | ||||||
| import ch.ethz.seb.sebserver.gbl.client.ProxyData; | import ch.ethz.seb.sebserver.gbl.client.ProxyData; | ||||||
|  | @ -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.gbl.util.Result; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; | import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.dao.LmsSetupDAO; | 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.LmsAPIService; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate; | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplateFactory; | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplateFactory; | ||||||
|  | @ -71,21 +73,9 @@ public class LmsAPIServiceImpl implements LmsAPIService { | ||||||
|         this.templateFactories = new EnumMap<>(factories); |         this.templateFactories = new EnumMap<>(factories); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** Listen to LmsSetupChangeEvent to release an affected LmsAPITemplate from cache |     @Override | ||||||
|      * |     public void cleanup() { | ||||||
|      * @param event the event holding the changed LmsSetup */ |         this.cache.clear(); | ||||||
|     @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 |     @Override | ||||||
|  | @ -107,10 +97,19 @@ public class LmsAPIServiceImpl implements LmsAPIService { | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public Result<LmsAPITemplate> getLmsAPITemplate(final String lmsSetupId) { |     public Result<LmsAPITemplate> getLmsAPITemplate(final String lmsSetupId) { | ||||||
|         return Result.tryCatch(() -> this.lmsSetupDAO |         return Result.tryCatch(() -> { | ||||||
|                 .byModelId(lmsSetupId) |             LmsAPITemplate lmsAPITemplate = getFromCache(lmsSetupId); | ||||||
|                 .getOrThrow()) |             if (lmsAPITemplate == null) { | ||||||
|                 .flatMap(this::getLmsAPITemplate); |                 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 |     @Override | ||||||
|  | @ -129,23 +128,22 @@ public class LmsAPIServiceImpl implements LmsAPIService { | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public LmsSetupTestResult testAdHoc(final LmsSetup lmsSetup) { |     public LmsSetupTestResult testAdHoc(final LmsSetup lmsSetup) { | ||||||
|         final ClientCredentials lmsCredentials = this.clientCredentialService.encryptClientCredentials( |         final AdHocAPITemplateDataSupplier apiTemplateDataSupplier = new AdHocAPITemplateDataSupplier( | ||||||
|                 lmsSetup.lmsAuthName, |                 lmsSetup, | ||||||
|                 lmsSetup.lmsAuthSecret, |                 this.clientCredentialService); | ||||||
|                 lmsSetup.lmsRestApiToken) |  | ||||||
|                 .getOrThrow(); |  | ||||||
| 
 | 
 | ||||||
|         final ProxyData proxyData = (StringUtils.isNoneBlank(lmsSetup.proxyHost)) |         final LmsAPITemplate lmsSetupTemplate = createLmsSetupTemplate(apiTemplateDataSupplier); | ||||||
|                 ? new ProxyData( |  | ||||||
|                         lmsSetup.proxyHost, |  | ||||||
|                         lmsSetup.proxyPort, |  | ||||||
|                         this.clientCredentialService.encryptClientCredentials( |  | ||||||
|                                 lmsSetup.proxyAuthUsername, |  | ||||||
|                                 lmsSetup.proxyAuthSecret) |  | ||||||
|                                 .getOrThrow()) |  | ||||||
|                 : null; |  | ||||||
| 
 | 
 | ||||||
|         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. |     /** Collect all QuizData from all affecting LmsSetup. | ||||||
|  | @ -183,6 +181,7 @@ public class LmsAPIServiceImpl implements LmsAPIService { | ||||||
|             return this.lmsSetupDAO.all(institutionId, true) |             return this.lmsSetupDAO.all(institutionId, true) | ||||||
|                     .getOrThrow() |                     .getOrThrow() | ||||||
|                     .parallelStream() |                     .parallelStream() | ||||||
|  |                     .map(LmsSetup::getModelId) | ||||||
|                     .map(this::getLmsAPITemplate) |                     .map(this::getLmsAPITemplate) | ||||||
|                     .flatMap(Result::onErrorLogAndSkip) |                     .flatMap(Result::onErrorLogAndSkip) | ||||||
|                     .map(template -> template.getQuizzes(filterMap)) |                     .map(template -> template.getQuizzes(filterMap)) | ||||||
|  | @ -193,18 +192,7 @@ public class LmsAPIServiceImpl implements LmsAPIService { | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private Result<LmsAPITemplate> getLmsAPITemplate(final LmsSetup lmsSetup) { |     private LmsAPITemplate getFromCache(final String lmsSetupId) { | ||||||
|         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) { |  | ||||||
|         // first cleanup the cache by removing old instances |         // first cleanup the cache by removing old instances | ||||||
|         final long currentTimeMillis = System.currentTimeMillis(); |         final long currentTimeMillis = System.currentTimeMillis(); | ||||||
|         new ArrayList<>(this.cache.keySet()) |         new ArrayList<>(this.cache.keySet()) | ||||||
|  | @ -212,40 +200,103 @@ public class LmsAPIServiceImpl implements LmsAPIService { | ||||||
|                 .filter(key -> key.creationTimestamp - currentTimeMillis > Constants.DAY_IN_MILLIS) |                 .filter(key -> key.creationTimestamp - currentTimeMillis > Constants.DAY_IN_MILLIS) | ||||||
|                 .forEach(this.cache::remove); |                 .forEach(this.cache::remove); | ||||||
|         // get from cache |         // 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()) { |         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 |         return createLmsSetupTemplate(new PersistentAPITemplateDataSupplier( | ||||||
|                 .getLmsAPIAccessCredentials(lmsSetup.getModelId()) |                 lmsSetupId, | ||||||
|  |                 this.lmsSetupDAO)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private LmsAPITemplate createLmsSetupTemplate(final APITemplateDataSupplier apiTemplateDataSupplier) { | ||||||
|  | 
 | ||||||
|  |         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(lmsType); | ||||||
|  | 
 | ||||||
|  |         return lmsAPITemplateFactory | ||||||
|  |                 .create(apiTemplateDataSupplier) | ||||||
|                 .getOrThrow(); |                 .getOrThrow(); | ||||||
| 
 |  | ||||||
|         final ProxyData proxyData = this.lmsSetupDAO |  | ||||||
|                 .getLmsAPIAccessProxyData(lmsSetup.getModelId()) |  | ||||||
|                 .getOr(null); |  | ||||||
| 
 |  | ||||||
|         return createLmsSetupTemplate(lmsSetup, credentials, proxyData); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private LmsAPITemplate createLmsSetupTemplate( |     /** 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 LmsSetup lmsSetup, | ||||||
|             final ClientCredentials credentials, |                 final ClientCredentialService clientCredentialService) { | ||||||
|             final ProxyData proxyData) { |             this.lmsSetup = lmsSetup; | ||||||
| 
 |             this.clientCredentialService = clientCredentialService; | ||||||
|         if (!this.templateFactories.containsKey(lmsSetup.lmsType)) { |  | ||||||
|             throw new UnsupportedOperationException("No support for LMS Type: " + lmsSetup.lmsType); |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         final LmsAPITemplateFactory lmsAPITemplateFactory = this.templateFactories.get(lmsSetup.lmsType); |         @Override | ||||||
|         return lmsAPITemplateFactory.create(lmsSetup, credentials, proxyData) |         public LmsSetup getLmsSetup() { | ||||||
|  |             return this.lmsSetup; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         @Override | ||||||
|  |         public ClientCredentials getLmsClientCredentials() { | ||||||
|  |             return this.clientCredentialService.encryptClientCredentials( | ||||||
|  |                     this.lmsSetup.getLmsAuthName(), | ||||||
|  |                     this.lmsSetup.getLmsAuthSecret()) | ||||||
|                     .getOrThrow(); |                     .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 { |     private static final class CacheKey { | ||||||
|         final String lmsSetupId; |         final String lmsSetupId; | ||||||
|         final long creationTimestamp; |         final long creationTimestamp; | ||||||
|  |  | ||||||
|  | @ -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; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| } |  | ||||||
|  | @ -18,6 +18,8 @@ import java.util.Map; | ||||||
| import java.util.Set; | import java.util.Set; | ||||||
| import java.util.stream.Collectors; | import java.util.stream.Collectors; | ||||||
| 
 | 
 | ||||||
|  | import javax.validation.constraints.NotNull; | ||||||
|  | 
 | ||||||
| import org.apache.commons.lang3.StringUtils; | import org.apache.commons.lang3.StringUtils; | ||||||
| import org.slf4j.Logger; | import org.slf4j.Logger; | ||||||
| import org.slf4j.LoggerFactory; | import org.slf4j.LoggerFactory; | ||||||
|  | @ -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.api.EntityType; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.exam.Exam; | 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.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.model.institution.LmsSetup.Features; | ||||||
| import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; | import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; | ||||||
| import ch.ethz.seb.sebserver.gbl.util.Result; | import ch.ethz.seb.sebserver.gbl.util.Result; | ||||||
|  | @ -62,15 +65,38 @@ public class SEBRestrictionServiceImpl implements SEBRestrictionService { | ||||||
|         this.examConfigService = examConfigService; |         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 |     @Override | ||||||
|     @Transactional |     @Transactional | ||||||
|     public Result<SEBRestriction> getSEBRestrictionFromExam(final Exam exam) { |     public Result<SEBRestriction> getSEBRestrictionFromExam(final Exam exam) { | ||||||
|         return Result.tryCatch(() -> { |         return Result.tryCatch(() -> { | ||||||
|             // load the config keys from restriction and merge with new generated config keys |             // load the config keys from restriction and merge with new generated config keys | ||||||
|  |             final long currentTimeMillis = System.currentTimeMillis(); | ||||||
|             final Set<String> configKeys = new HashSet<>(); |             final Set<String> configKeys = new HashSet<>(); | ||||||
|             final Collection<String> generatedKeys = this.examConfigService |             final Collection<String> generatedKeys = this.examConfigService | ||||||
|                     .generateConfigKeys(exam.institutionId, exam.id) |                     .generateConfigKeys(exam.institutionId, exam.id) | ||||||
|                     .getOrThrow(); |                     .getOrThrow(); | ||||||
|  |             System.out.println("******* " + (System.currentTimeMillis() - currentTimeMillis)); | ||||||
| 
 | 
 | ||||||
|             configKeys.addAll(generatedKeys); |             configKeys.addAll(generatedKeys); | ||||||
|             if (generatedKeys != null && !generatedKeys.isEmpty()) { |             if (generatedKeys != null && !generatedKeys.isEmpty()) { | ||||||
|  | @ -134,6 +160,7 @@ public class SEBRestrictionServiceImpl implements SEBRestrictionService { | ||||||
|                     null, null, null, null, null, null, null, null, null, null, |                     null, null, null, null, null, null, null, null, null, null, | ||||||
|                     exam.supporter, |                     exam.supporter, | ||||||
|                     exam.status, |                     exam.status, | ||||||
|  |                     null, | ||||||
|                     (browserExamKeys != null && !browserExamKeys.isEmpty()) |                     (browserExamKeys != null && !browserExamKeys.isEmpty()) | ||||||
|                             ? StringUtils.join(browserExamKeys, Constants.LIST_SEPARATOR_CHAR) |                             ? StringUtils.join(browserExamKeys, Constants.LIST_SEPARATOR_CHAR) | ||||||
|                             : StringUtils.EMPTY, |                             : StringUtils.EMPTY, | ||||||
|  | @ -167,11 +194,12 @@ public class SEBRestrictionServiceImpl implements SEBRestrictionService { | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public Result<Exam> applySEBClientRestriction(final Exam exam) { |     public Result<Exam> applySEBClientRestriction(final Exam exam) { | ||||||
|  |         return Result.tryCatch(() -> { | ||||||
|             if (!this.lmsAPIService |             if (!this.lmsAPIService | ||||||
|                     .getLmsSetup(exam.lmsSetupId) |                     .getLmsSetup(exam.lmsSetupId) | ||||||
|                     .getOrThrow().lmsType.features.contains(Features.SEB_RESTRICTION)) { |                     .getOrThrow().lmsType.features.contains(Features.SEB_RESTRICTION)) { | ||||||
| 
 | 
 | ||||||
|             return Result.of(exam); |                 return exam; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             return this.getSEBRestrictionFromExam(exam) |             return this.getSEBRestrictionFromExam(exam) | ||||||
|  | @ -188,6 +216,8 @@ public class SEBRestrictionServiceImpl implements SEBRestrictionService { | ||||||
|                                         sebRestrictionData)) |                                         sebRestrictionData)) | ||||||
|                                 .map(data -> exam) |                                 .map(data -> exam) | ||||||
|                                 .getOrThrow(); |                                 .getOrThrow(); | ||||||
|  |                     }) | ||||||
|  |                     .getOrThrow(); | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -10,6 +10,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.edx; | ||||||
| 
 | 
 | ||||||
| import java.net.URL; | import java.net.URL; | ||||||
| import java.util.ArrayList; | import java.util.ArrayList; | ||||||
|  | import java.util.Arrays; | ||||||
| import java.util.HashMap; | import java.util.HashMap; | ||||||
| import java.util.List; | import java.util.List; | ||||||
| import java.util.Map; | 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.gbl.util.Utils; | ||||||
| import ch.ethz.seb.sebserver.webservice.WebserviceInfo; | import ch.ethz.seb.sebserver.webservice.WebserviceInfo; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; | import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.lms.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. | /** Implements the LmsAPITemplate for Open edX LMS Course API access. | ||||||
|  * |  * | ||||||
|  * See also: https://course-catalog-api-guide.readthedocs.io */ |  * See also: https://course-catalog-api-guide.readthedocs.io */ | ||||||
| final class OpenEdxCourseAccess extends CourseAccess { | final class OpenEdxCourseAccess extends AbstractCourseAccess { | ||||||
| 
 | 
 | ||||||
|     private static final Logger log = LoggerFactory.getLogger(OpenEdxCourseAccess.class); |     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 static final String OPEN_EDX_DEFAULT_USER_PROFILE_ENDPOINT = "/api/user/v1/accounts?username="; | ||||||
| 
 | 
 | ||||||
|     private final JSONMapper jsonMapper; |     private final JSONMapper jsonMapper; | ||||||
|     private final LmsSetup lmsSetup; |  | ||||||
|     private final OpenEdxRestTemplateFactory openEdxRestTemplateFactory; |     private final OpenEdxRestTemplateFactory openEdxRestTemplateFactory; | ||||||
|     private final WebserviceInfo webserviceInfo; |     private final WebserviceInfo webserviceInfo; | ||||||
|     private final MemoizingCircuitBreaker<List<QuizData>> allQuizzesRequest; |     private final MemoizingCircuitBreaker<List<QuizData>> allQuizzesRequest; | ||||||
|  | @ -80,7 +81,6 @@ final class OpenEdxCourseAccess extends CourseAccess { | ||||||
| 
 | 
 | ||||||
|     public OpenEdxCourseAccess( |     public OpenEdxCourseAccess( | ||||||
|             final JSONMapper jsonMapper, |             final JSONMapper jsonMapper, | ||||||
|             final LmsSetup lmsSetup, |  | ||||||
|             final OpenEdxRestTemplateFactory openEdxRestTemplateFactory, |             final OpenEdxRestTemplateFactory openEdxRestTemplateFactory, | ||||||
|             final WebserviceInfo webserviceInfo, |             final WebserviceInfo webserviceInfo, | ||||||
|             final AsyncService asyncService, |             final AsyncService asyncService, | ||||||
|  | @ -88,7 +88,6 @@ final class OpenEdxCourseAccess extends CourseAccess { | ||||||
| 
 | 
 | ||||||
|         super(asyncService, environment); |         super(asyncService, environment); | ||||||
|         this.jsonMapper = jsonMapper; |         this.jsonMapper = jsonMapper; | ||||||
|         this.lmsSetup = lmsSetup; |  | ||||||
|         this.openEdxRestTemplateFactory = openEdxRestTemplateFactory; |         this.openEdxRestTemplateFactory = openEdxRestTemplateFactory; | ||||||
|         this.webserviceInfo = webserviceInfo; |         this.webserviceInfo = webserviceInfo; | ||||||
| 
 | 
 | ||||||
|  | @ -130,8 +129,14 @@ final class OpenEdxCourseAccess extends CourseAccess { | ||||||
|         }; |         }; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     APITemplateDataSupplier getApiTemplateDataSupplier() { | ||||||
|  |         return this.openEdxRestTemplateFactory.apiTemplateDataSupplier; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     LmsSetupTestResult initAPIAccess() { |     LmsSetupTestResult initAPIAccess() { | ||||||
| 
 | 
 | ||||||
|  |         final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup(); | ||||||
|  | 
 | ||||||
|         final LmsSetupTestResult attributesCheck = this.openEdxRestTemplateFactory.test(); |         final LmsSetupTestResult attributesCheck = this.openEdxRestTemplateFactory.test(); | ||||||
|         if (!attributesCheck.isOk()) { |         if (!attributesCheck.isOk()) { | ||||||
|             return attributesCheck; |             return attributesCheck; | ||||||
|  | @ -148,13 +153,14 @@ final class OpenEdxCourseAccess extends CourseAccess { | ||||||
|         final OAuth2RestTemplate restTemplate = restTemplateRequest.get(); |         final OAuth2RestTemplate restTemplate = restTemplateRequest.get(); | ||||||
| 
 | 
 | ||||||
|         try { |         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) { |         } catch (final RuntimeException e) { | ||||||
| 
 | 
 | ||||||
|             restTemplate.setAuthenticator(new EdxOAuth2RequestAuthenticator()); |             restTemplate.setAuthenticator(new EdxOAuth2RequestAuthenticator()); | ||||||
| 
 | 
 | ||||||
|             try { |             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) { |             } catch (final RuntimeException ee) { | ||||||
|                 log.error("Failed to access Open edX course API: ", ee); |                 log.error("Failed to access Open edX course API: ", ee); | ||||||
|                 return LmsSetupTestResult.ofQuizAccessAPIError(ee.getMessage()); |                 return LmsSetupTestResult.ofQuizAccessAPIError(ee.getMessage()); | ||||||
|  | @ -167,14 +173,18 @@ final class OpenEdxCourseAccess extends CourseAccess { | ||||||
|     @Override |     @Override | ||||||
|     public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeSessionId) { |     public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeSessionId) { | ||||||
|         return Result.tryCatch(() -> { |         return Result.tryCatch(() -> { | ||||||
|  |             final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup(); | ||||||
|  |             final HttpHeaders httpHeaders = new HttpHeaders(); | ||||||
|             final OAuth2RestTemplate template = getRestTemplate() |             final OAuth2RestTemplate template = getRestTemplate() | ||||||
|                     .getOrThrow(); |                     .getOrThrow(); | ||||||
| 
 | 
 | ||||||
|             final String externalStartURI = this.webserviceInfo.getLmsExternalAddressAlias(this.lmsSetup.lmsApiUrl); |             final String externalStartURI = this.webserviceInfo | ||||||
|  |                     .getLmsExternalAddressAlias(lmsSetup.lmsApiUrl); | ||||||
|  | 
 | ||||||
|             final String uri = (externalStartURI != null) |             final String uri = (externalStartURI != null) | ||||||
|                     ? externalStartURI + OPEN_EDX_DEFAULT_USER_PROFILE_ENDPOINT + examineeSessionId |                     ? externalStartURI + OPEN_EDX_DEFAULT_USER_PROFILE_ENDPOINT + examineeSessionId | ||||||
|                     : this.lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_USER_PROFILE_ENDPOINT + examineeSessionId; |                     : lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_USER_PROFILE_ENDPOINT + examineeSessionId; | ||||||
|             final HttpHeaders httpHeaders = new HttpHeaders(); | 
 | ||||||
|             final String responseJSON = template.exchange( |             final String responseJSON = template.exchange( | ||||||
|                     uri, |                     uri, | ||||||
|                     HttpMethod.GET, |                     HttpMethod.GET, | ||||||
|  | @ -211,10 +221,25 @@ final class OpenEdxCourseAccess extends CourseAccess { | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     protected Supplier<List<QuizData>> quizzesSupplier(final Set<String> ids) { |     protected Supplier<List<QuizData>> quizzesSupplier(final Set<String> ids) { | ||||||
|  | 
 | ||||||
|  |         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() |             return () -> getRestTemplate() | ||||||
|                     .map(template -> this.collectQuizzes(template, ids)) |                     .map(template -> this.collectQuizzes(template, ids)) | ||||||
|                     .getOrThrow(); |                     .getOrThrow(); | ||||||
|         } |         } | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     private Supplier<List<QuizData>> quizzesSupplier() { |     private Supplier<List<QuizData>> quizzesSupplier() { | ||||||
|         return () -> getRestTemplate() |         return () -> getRestTemplate() | ||||||
|  | @ -225,8 +250,10 @@ final class OpenEdxCourseAccess extends CourseAccess { | ||||||
|     @Override |     @Override | ||||||
|     protected Supplier<Chapters> getCourseChaptersSupplier(final String courseId) { |     protected Supplier<Chapters> getCourseChaptersSupplier(final String courseId) { | ||||||
|         return () -> { |         return () -> { | ||||||
|  |             final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup(); | ||||||
|  | 
 | ||||||
|             final String uri = |             final String uri = | ||||||
|                     this.lmsSetup.lmsApiUrl + |                     lmsSetup.lmsApiUrl + | ||||||
|                             OPEN_EDX_DEFAULT_BLOCKS_ENDPOINT + |                             OPEN_EDX_DEFAULT_BLOCKS_ENDPOINT + | ||||||
|                             Utils.encodeFormURL_UTF_8(courseId); |                             Utils.encodeFormURL_UTF_8(courseId); | ||||||
|             return new Chapters(getCourseBlocks(uri) |             return new Chapters(getCourseBlocks(uri) | ||||||
|  | @ -239,16 +266,18 @@ final class OpenEdxCourseAccess extends CourseAccess { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private ArrayList<QuizData> collectQuizzes(final OAuth2RestTemplate restTemplate, final Set<String> ids) { |     private ArrayList<QuizData> collectQuizzes(final OAuth2RestTemplate restTemplate, final Set<String> ids) { | ||||||
|         final String externalStartURI = getExternalLMSServerAddress(this.lmsSetup); |         final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup(); | ||||||
|  |         final String externalStartURI = getExternalLMSServerAddress(lmsSetup); | ||||||
|  | 
 | ||||||
|         return collectCourses( |         return collectCourses( | ||||||
|                 this.lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_ENDPOINT, |                 lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_ENDPOINT, | ||||||
|                 restTemplate, |                 restTemplate, | ||||||
|                 ids) |                 ids) | ||||||
|                         .stream() |                         .stream() | ||||||
|                         .reduce( |                         .reduce( | ||||||
|                                 new ArrayList<>(), |                                 new ArrayList<>(), | ||||||
|                                 (list, courseData) -> { |                                 (list, courseData) -> { | ||||||
|                                     list.add(quizDataOf(this.lmsSetup, courseData, externalStartURI)); |                                     list.add(quizDataOf(lmsSetup, courseData, externalStartURI)); | ||||||
|                                     return list; |                                     return list; | ||||||
|                                 }, |                                 }, | ||||||
|                                 (list1, list2) -> { |                                 (list1, list2) -> { | ||||||
|  | @ -258,15 +287,16 @@ final class OpenEdxCourseAccess extends CourseAccess { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private ArrayList<QuizData> collectAllQuizzes(final OAuth2RestTemplate restTemplate) { |     private ArrayList<QuizData> collectAllQuizzes(final OAuth2RestTemplate restTemplate) { | ||||||
|         final String externalStartURI = getExternalLMSServerAddress(this.lmsSetup); |         final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup(); | ||||||
|  |         final String externalStartURI = getExternalLMSServerAddress(lmsSetup); | ||||||
|         return collectAllCourses( |         return collectAllCourses( | ||||||
|                 this.lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_ENDPOINT, |                 lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_ENDPOINT, | ||||||
|                 restTemplate) |                 restTemplate) | ||||||
|                         .stream() |                         .stream() | ||||||
|                         .reduce( |                         .reduce( | ||||||
|                                 new ArrayList<>(), |                                 new ArrayList<>(), | ||||||
|                                 (list, courseData) -> { |                                 (list, courseData) -> { | ||||||
|                                     list.add(quizDataOf(this.lmsSetup, courseData, externalStartURI)); |                                     list.add(quizDataOf(lmsSetup, courseData, externalStartURI)); | ||||||
|                                     return list; |                                     return list; | ||||||
|                                 }, |                                 }, | ||||||
|                                 (list1, list2) -> { |                                 (list1, list2) -> { | ||||||
|  | @ -322,6 +352,21 @@ final class OpenEdxCourseAccess extends CourseAccess { | ||||||
|         return collector; |         return collector; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     private CourseData getOneCourses( | ||||||
|  |             final String pageURI, | ||||||
|  |             final OAuth2RestTemplate restTemplate, | ||||||
|  |             final String id) { | ||||||
|  | 
 | ||||||
|  |         final HttpHeaders httpHeaders = new HttpHeaders(); | ||||||
|  |         final ResponseEntity<CourseData> exchange = restTemplate.exchange( | ||||||
|  |                 pageURI + "/" + id, | ||||||
|  |                 HttpMethod.GET, | ||||||
|  |                 new HttpEntity<>(httpHeaders), | ||||||
|  |                 CourseData.class); | ||||||
|  | 
 | ||||||
|  |         return exchange.getBody(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     private List<CourseData> collectAllCourses(final String pageURI, final OAuth2RestTemplate restTemplate) { |     private List<CourseData> collectAllCourses(final String pageURI, final OAuth2RestTemplate restTemplate) { | ||||||
|         final List<CourseData> collector = new ArrayList<>(); |         final List<CourseData> collector = new ArrayList<>(); | ||||||
|         EdXPage page = getEdxPage(pageURI, restTemplate).getBody(); |         EdXPage page = getEdxPage(pageURI, restTemplate).getBody(); | ||||||
|  |  | ||||||
|  | @ -8,8 +8,6 @@ | ||||||
| 
 | 
 | ||||||
| package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.edx; | package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.edx; | ||||||
| 
 | 
 | ||||||
| import java.util.function.BooleanSupplier; |  | ||||||
| 
 |  | ||||||
| import org.slf4j.Logger; | import org.slf4j.Logger; | ||||||
| import org.slf4j.LoggerFactory; | import org.slf4j.LoggerFactory; | ||||||
| import org.springframework.http.HttpEntity; | import org.springframework.http.HttpEntity; | ||||||
|  | @ -23,8 +21,6 @@ import org.springframework.web.client.HttpClientErrorException; | ||||||
| 
 | 
 | ||||||
| import com.fasterxml.jackson.core.JsonProcessingException; | import com.fasterxml.jackson.core.JsonProcessingException; | ||||||
| 
 | 
 | ||||||
| import ch.ethz.seb.sebserver.gbl.api.APIMessage; |  | ||||||
| import ch.ethz.seb.sebserver.gbl.api.APIMessage.APIMessageException; |  | ||||||
| import ch.ethz.seb.sebserver.gbl.api.JSONMapper; | import ch.ethz.seb.sebserver.gbl.api.JSONMapper; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.exam.OpenEdxSEBRestriction; | import ch.ethz.seb.sebserver.gbl.model.exam.OpenEdxSEBRestriction; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; | 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 = |     private static final String OPEN_EDX_DEFAULT_COURSE_RESTRICTION_API_PATH = | ||||||
|             "/seb-openedx/api/v1/course/%s/configuration/"; |             "/seb-openedx/api/v1/course/%s/configuration/"; | ||||||
| 
 | 
 | ||||||
|     private final LmsSetup lmsSetup; |  | ||||||
|     private final JSONMapper jsonMapper; |     private final JSONMapper jsonMapper; | ||||||
|     private final OpenEdxRestTemplateFactory openEdxRestTemplateFactory; |     private final OpenEdxRestTemplateFactory openEdxRestTemplateFactory; | ||||||
| 
 | 
 | ||||||
|     private OAuth2RestTemplate restTemplate; |     private OAuth2RestTemplate restTemplate; | ||||||
| 
 | 
 | ||||||
|     protected OpenEdxCourseRestriction( |     protected OpenEdxCourseRestriction( | ||||||
|             final LmsSetup lmsSetup, |  | ||||||
|             final JSONMapper jsonMapper, |             final JSONMapper jsonMapper, | ||||||
|             final OpenEdxRestTemplateFactory openEdxRestTemplateFactory, |             final OpenEdxRestTemplateFactory openEdxRestTemplateFactory, | ||||||
|             final int restrictionAPIPushCount) { |             final int restrictionAPIPushCount) { | ||||||
| 
 | 
 | ||||||
|         this.lmsSetup = lmsSetup; |  | ||||||
|         this.jsonMapper = jsonMapper; |         this.jsonMapper = jsonMapper; | ||||||
|         this.openEdxRestTemplateFactory = openEdxRestTemplateFactory; |         this.openEdxRestTemplateFactory = openEdxRestTemplateFactory; | ||||||
|     } |     } | ||||||
|  | @ -75,6 +68,8 @@ public class OpenEdxCourseRestriction { | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         final OAuth2RestTemplate restTemplate = restTemplateRequest.get(); |         final OAuth2RestTemplate restTemplate = restTemplateRequest.get(); | ||||||
|  |         try { | ||||||
|  |             final LmsSetup lmsSetup = this.openEdxRestTemplateFactory.apiTemplateDataSupplier.getLmsSetup(); | ||||||
| 
 | 
 | ||||||
|             // NOTE: since the OPEN_EDX_DEFAULT_COURSE_RESTRICTION_API_INFO endpoint is |             // NOTE: since the OPEN_EDX_DEFAULT_COURSE_RESTRICTION_API_INFO endpoint is | ||||||
|             //       not accessible within OAuth2 authentication (just with user - authentication), |             //       not accessible within OAuth2 authentication (just with user - authentication), | ||||||
|  | @ -82,8 +77,7 @@ public class OpenEdxCourseRestriction { | ||||||
|             //       if there is no 404 response. |             //       if there is no 404 response. | ||||||
|             // TODO: Ask eduNEXT to implement also OAuth2 API access for this endpoint to be able |             // TODO: Ask eduNEXT to implement also OAuth2 API access for this endpoint to be able | ||||||
|             //       to check the version of the installed plugin. |             //       to check the version of the installed plugin. | ||||||
|         final String url = this.lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_RESTRICTION_API_INFO; |             final String url = lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_RESTRICTION_API_INFO; | ||||||
|         try { |  | ||||||
| 
 | 
 | ||||||
|             restTemplate.exchange( |             restTemplate.exchange( | ||||||
|                     url, |                     url, | ||||||
|  | @ -111,8 +105,10 @@ public class OpenEdxCourseRestriction { | ||||||
|             log.debug("GET SEB Client restriction on course: {}", courseId); |             log.debug("GET SEB Client restriction on course: {}", courseId); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         final LmsSetup lmsSetup = this.openEdxRestTemplateFactory.apiTemplateDataSupplier.getLmsSetup(); | ||||||
|  | 
 | ||||||
|         return Result.tryCatch(() -> { |         return Result.tryCatch(() -> { | ||||||
|             final String url = this.lmsSetup.lmsApiUrl + getSEBRestrictionUrl(courseId); |             final String url = lmsSetup.lmsApiUrl + getSEBRestrictionUrl(courseId); | ||||||
|             final HttpHeaders httpHeaders = new HttpHeaders(); |             final HttpHeaders httpHeaders = new HttpHeaders(); | ||||||
|             httpHeaders.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); |             httpHeaders.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); | ||||||
|             httpHeaders.add(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate"); |             httpHeaders.add(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate"); | ||||||
|  | @ -146,30 +142,16 @@ public class OpenEdxCourseRestriction { | ||||||
|             log.debug("PUT SEB Client restriction on course: {} : {}", courseId, restriction); |             log.debug("PUT SEB Client restriction on course: {} : {}", courseId, restriction); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return handleSEBRestriction(pushSEBRestrictionFunction( |         return Result.tryCatch(() -> { | ||||||
|                 restriction, |             final LmsSetup lmsSetup = this.openEdxRestTemplateFactory.apiTemplateDataSupplier.getLmsSetup(); | ||||||
|                 courseId)); |             final String url = lmsSetup.lmsApiUrl + getSEBRestrictionUrl(courseId); | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     Result<Boolean> deleteSEBRestriction(final String courseId) { |  | ||||||
| 
 |  | ||||||
|         if (log.isDebugEnabled()) { |  | ||||||
|             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(); |             final HttpHeaders httpHeaders = new HttpHeaders(); | ||||||
|             httpHeaders.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); |             httpHeaders.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); | ||||||
|             httpHeaders.add(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate"); |             httpHeaders.add(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate"); | ||||||
|         return () -> { |             final OpenEdxSEBRestriction body = this | ||||||
|             final OpenEdxSEBRestriction body = this.restTemplate.exchange( |                     .getRestTemplate() | ||||||
|  |                     .getOrThrow() | ||||||
|  |                     .exchange( | ||||||
|                             url, |                             url, | ||||||
|                             HttpMethod.PUT, |                             HttpMethod.PUT, | ||||||
|                             new HttpEntity<>(toJson(restriction), httpHeaders), |                             new HttpEntity<>(toJson(restriction), httpHeaders), | ||||||
|  | @ -181,16 +163,24 @@ public class OpenEdxCourseRestriction { | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             return true; |             return true; | ||||||
|         }; |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private BooleanSupplier deleteSEBRestrictionFunction(final String courseId) { |     Result<Boolean> deleteSEBRestriction(final String courseId) { | ||||||
| 
 | 
 | ||||||
|         final String url = this.lmsSetup.lmsApiUrl + getSEBRestrictionUrl(courseId); |         if (log.isDebugEnabled()) { | ||||||
|         return () -> { |             log.debug("DELETE SEB Client restriction on course: {}", courseId); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return Result.tryCatch(() -> { | ||||||
|  |             final LmsSetup lmsSetup = this.openEdxRestTemplateFactory.apiTemplateDataSupplier.getLmsSetup(); | ||||||
|  |             final String url = lmsSetup.lmsApiUrl + getSEBRestrictionUrl(courseId); | ||||||
|             final HttpHeaders httpHeaders = new HttpHeaders(); |             final HttpHeaders httpHeaders = new HttpHeaders(); | ||||||
|             httpHeaders.add(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate"); |             httpHeaders.add(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate"); | ||||||
|             final ResponseEntity<Object> exchange = this.restTemplate.exchange( |             final ResponseEntity<Object> exchange = this | ||||||
|  |                     .getRestTemplate() | ||||||
|  |                     .getOrThrow() | ||||||
|  |                     .exchange( | ||||||
|                             url, |                             url, | ||||||
|                             HttpMethod.DELETE, |                             HttpMethod.DELETE, | ||||||
|                             new HttpEntity<>(httpHeaders), |                             new HttpEntity<>(httpHeaders), | ||||||
|  | @ -200,32 +190,82 @@ public class OpenEdxCourseRestriction { | ||||||
|                 if (log.isDebugEnabled()) { |                 if (log.isDebugEnabled()) { | ||||||
|                     log.debug("Successfully PUT SEB Client restriction on course: {}", courseId); |                     log.debug("Successfully PUT SEB Client restriction on course: {}", courseId); | ||||||
|                 } |                 } | ||||||
|             } else { |  | ||||||
|                 log.error("Unexpected response for deletion: {}", exchange); |  | ||||||
|                 return false; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|                 return true; |                 return true; | ||||||
|         }; |             } else { | ||||||
|     } |                 throw new RuntimeException("Unexpected response for deletion: " + exchange); | ||||||
| 
 |  | ||||||
|     private Result<Boolean> handleSEBRestriction(final BooleanSupplier task) { |  | ||||||
|         return getRestTemplate() |  | ||||||
|                 .map(restTemplate -> { |  | ||||||
|                     try { |  | ||||||
|                         return task.getAsBoolean(); |  | ||||||
|                     } catch (final HttpClientErrorException ce) { |  | ||||||
|                         if (ce.getStatusCode() == HttpStatus.UNAUTHORIZED) { |  | ||||||
|                             throw new APIMessageException(APIMessage.ErrorMessage.UNAUTHORIZED.of(ce.getMessage() |  | ||||||
|                                     + " Unable to get access for API. Please check the corresponding LMS Setup ")); |  | ||||||
|                         } |  | ||||||
|                         throw ce; |  | ||||||
|                     } catch (final Exception e) { |  | ||||||
|                         throw new RuntimeException("Unexpected: ", e); |  | ||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
|  | 
 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | //    private BooleanSupplier pushSEBRestrictionFunction( | ||||||
|  | //            final OpenEdxSEBRestriction restriction, | ||||||
|  | //            final String courseId) { | ||||||
|  | // | ||||||
|  | //        final LmsSetup lmsSetup = this.openEdxRestTemplateFactory.apiTemplateDataSupplier.getLmsSetup(); | ||||||
|  | //        final String url = lmsSetup.lmsApiUrl + getSEBRestrictionUrl(courseId); | ||||||
|  | //        final HttpHeaders httpHeaders = new HttpHeaders(); | ||||||
|  | //        httpHeaders.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); | ||||||
|  | //        httpHeaders.add(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate"); | ||||||
|  | //        return () -> { | ||||||
|  | //            final OpenEdxSEBRestriction body = this.restTemplate.exchange( | ||||||
|  | //                    url, | ||||||
|  | //                    HttpMethod.PUT, | ||||||
|  | //                    new HttpEntity<>(toJson(restriction), httpHeaders), | ||||||
|  | //                    OpenEdxSEBRestriction.class) | ||||||
|  | //                    .getBody(); | ||||||
|  | // | ||||||
|  | //            if (log.isDebugEnabled()) { | ||||||
|  | //                log.debug("Successfully PUT SEB Client restriction on course: {} : {}", courseId, body); | ||||||
|  | //            } | ||||||
|  | // | ||||||
|  | //            return true; | ||||||
|  | //        }; | ||||||
|  | //    } | ||||||
|  | 
 | ||||||
|  | //    private BooleanSupplier deleteSEBRestrictionFunction(final String courseId) { | ||||||
|  | // | ||||||
|  | //        final LmsSetup lmsSetup = this.openEdxRestTemplateFactory.apiTemplateDataSupplier.getLmsSetup(); | ||||||
|  | //        final String url = lmsSetup.lmsApiUrl + getSEBRestrictionUrl(courseId); | ||||||
|  | //        return () -> { | ||||||
|  | //            final HttpHeaders httpHeaders = new HttpHeaders(); | ||||||
|  | //            httpHeaders.add(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate"); | ||||||
|  | //            final ResponseEntity<Object> exchange = this.restTemplate.exchange( | ||||||
|  | //                    url, | ||||||
|  | //                    HttpMethod.DELETE, | ||||||
|  | //                    new HttpEntity<>(httpHeaders), | ||||||
|  | //                    Object.class); | ||||||
|  | // | ||||||
|  | //            if (exchange.getStatusCode() == HttpStatus.NO_CONTENT) { | ||||||
|  | //                if (log.isDebugEnabled()) { | ||||||
|  | //                    log.debug("Successfully PUT SEB Client restriction on course: {}", courseId); | ||||||
|  | //                } | ||||||
|  | //            } else { | ||||||
|  | //                log.error("Unexpected response for deletion: {}", exchange); | ||||||
|  | //                return false; | ||||||
|  | //            } | ||||||
|  | // | ||||||
|  | //            return true; | ||||||
|  | //        }; | ||||||
|  | //    } | ||||||
|  | 
 | ||||||
|  | //    private Result<Boolean> handleSEBRestriction(final BooleanSupplier task) { | ||||||
|  | //        return getRestTemplate() | ||||||
|  | //                .map(restTemplate -> { | ||||||
|  | //                    try { | ||||||
|  | //                        return task.getAsBoolean(); | ||||||
|  | //                    } catch (final HttpClientErrorException ce) { | ||||||
|  | //                        if (ce.getStatusCode() == HttpStatus.UNAUTHORIZED) { | ||||||
|  | //                            throw new APIMessageException(APIMessage.ErrorMessage.UNAUTHORIZED.of(ce.getMessage() | ||||||
|  | //                                    + " Unable to get access for API. Please check the corresponding LMS Setup ")); | ||||||
|  | //                        } | ||||||
|  | //                        throw ce; | ||||||
|  | //                    } catch (final Exception e) { | ||||||
|  | //                        throw new RuntimeException("Unexpected: ", e); | ||||||
|  | //                    } | ||||||
|  | //                }); | ||||||
|  | //    } | ||||||
|  | 
 | ||||||
|     private String getSEBRestrictionUrl(final String courseId) { |     private String getSEBRestrictionUrl(final String courseId) { | ||||||
|         return String.format(OPEN_EDX_DEFAULT_COURSE_RESTRICTION_API_PATH, courseId); |         return String.format(OPEN_EDX_DEFAULT_COURSE_RESTRICTION_API_PATH, courseId); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -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.QuizData; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction; | import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; | import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; | ||||||
|  | import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult; | import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails; | import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails; | ||||||
| import ch.ethz.seb.sebserver.gbl.util.Result; | 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 static final Logger log = LoggerFactory.getLogger(OpenEdxLmsAPITemplate.class); | ||||||
| 
 | 
 | ||||||
|     private final LmsSetup lmsSetup; |  | ||||||
|     private final OpenEdxCourseAccess openEdxCourseAccess; |     private final OpenEdxCourseAccess openEdxCourseAccess; | ||||||
|     private final OpenEdxCourseRestriction openEdxCourseRestriction; |     private final OpenEdxCourseRestriction openEdxCourseRestriction; | ||||||
| 
 | 
 | ||||||
|     OpenEdxLmsAPITemplate( |     OpenEdxLmsAPITemplate( | ||||||
|             final LmsSetup lmsSetup, |  | ||||||
|             final OpenEdxCourseAccess openEdxCourseAccess, |             final OpenEdxCourseAccess openEdxCourseAccess, | ||||||
|             final OpenEdxCourseRestriction openEdxCourseRestriction) { |             final OpenEdxCourseRestriction openEdxCourseRestriction) { | ||||||
| 
 | 
 | ||||||
|         this.lmsSetup = lmsSetup; |  | ||||||
|         this.openEdxCourseAccess = openEdxCourseAccess; |         this.openEdxCourseAccess = openEdxCourseAccess; | ||||||
|         this.openEdxCourseRestriction = openEdxCourseRestriction; |         this.openEdxCourseRestriction = openEdxCourseRestriction; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     @Override | ||||||
|  |     public LmsType getType() { | ||||||
|  |         return LmsType.OPEN_EDX; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     @Override |     @Override | ||||||
|     public LmsSetup lmsSetup() { |     public LmsSetup lmsSetup() { | ||||||
|         return this.lmsSetup; |         return this.openEdxCourseAccess | ||||||
|  |                 .getApiTemplateDataSupplier() | ||||||
|  |                 .getLmsSetup(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|  | @ -150,7 +155,8 @@ final class OpenEdxLmsAPITemplate implements LmsAPITemplate { | ||||||
|             log.debug("Release SEB Client restriction for Exam: {}", exam); |             log.debug("Release SEB Client restriction for Exam: {}", exam); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return this.openEdxCourseRestriction.deleteSEBRestriction(exam.externalId) |         return this.openEdxCourseRestriction | ||||||
|  |                 .deleteSEBRestriction(exam.externalId) | ||||||
|                 .map(result -> exam); |                 .map(result -> exam); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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.api.JSONMapper; | ||||||
| import ch.ethz.seb.sebserver.gbl.async.AsyncService; | import ch.ethz.seb.sebserver.gbl.async.AsyncService; | ||||||
| import ch.ethz.seb.sebserver.gbl.client.ClientCredentialService; | import ch.ethz.seb.sebserver.gbl.client.ClientCredentialService; | ||||||
| import ch.ethz.seb.sebserver.gbl.client.ClientCredentials; |  | ||||||
| import ch.ethz.seb.sebserver.gbl.client.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.model.institution.LmsSetup.LmsType; | ||||||
| import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; | import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; | ||||||
| import ch.ethz.seb.sebserver.gbl.util.Result; | import ch.ethz.seb.sebserver.gbl.util.Result; | ||||||
| import ch.ethz.seb.sebserver.webservice.WebserviceInfo; | 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.LmsAPITemplate; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplateFactory; | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplateFactory; | ||||||
| 
 | 
 | ||||||
|  | @ -71,37 +69,29 @@ public class OpenEdxLmsAPITemplateFactory implements LmsAPITemplateFactory { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public Result<LmsAPITemplate> create( |     public Result<LmsAPITemplate> create(final APITemplateDataSupplier apiTemplateDataSupplier) { | ||||||
|             final LmsSetup lmsSetup, |  | ||||||
|             final ClientCredentials credentials, |  | ||||||
|             final ProxyData proxyData) { |  | ||||||
| 
 | 
 | ||||||
|         return Result.tryCatch(() -> { |         return Result.tryCatch(() -> { | ||||||
| 
 | 
 | ||||||
|             final OpenEdxRestTemplateFactory openEdxRestTemplateFactory = new OpenEdxRestTemplateFactory( |             final OpenEdxRestTemplateFactory openEdxRestTemplateFactory = new OpenEdxRestTemplateFactory( | ||||||
|                     lmsSetup, |                     apiTemplateDataSupplier, | ||||||
|                     credentials, |  | ||||||
|                     proxyData, |  | ||||||
|                     this.clientCredentialService, |                     this.clientCredentialService, | ||||||
|                     this.clientHttpRequestFactoryService, |                     this.clientHttpRequestFactoryService, | ||||||
|                     this.alternativeTokenRequestPaths); |                     this.alternativeTokenRequestPaths); | ||||||
| 
 | 
 | ||||||
|             final OpenEdxCourseAccess openEdxCourseAccess = new OpenEdxCourseAccess( |             final OpenEdxCourseAccess openEdxCourseAccess = new OpenEdxCourseAccess( | ||||||
|                     this.jsonMapper, |                     this.jsonMapper, | ||||||
|                     lmsSetup, |  | ||||||
|                     openEdxRestTemplateFactory, |                     openEdxRestTemplateFactory, | ||||||
|                     this.webserviceInfo, |                     this.webserviceInfo, | ||||||
|                     this.asyncService, |                     this.asyncService, | ||||||
|                     this.environment); |                     this.environment); | ||||||
| 
 | 
 | ||||||
|             final OpenEdxCourseRestriction openEdxCourseRestriction = new OpenEdxCourseRestriction( |             final OpenEdxCourseRestriction openEdxCourseRestriction = new OpenEdxCourseRestriction( | ||||||
|                     lmsSetup, |  | ||||||
|                     this.jsonMapper, |                     this.jsonMapper, | ||||||
|                     openEdxRestTemplateFactory, |                     openEdxRestTemplateFactory, | ||||||
|                     this.restrictionAPIPushCount); |                     this.restrictionAPIPushCount); | ||||||
| 
 | 
 | ||||||
|             return new OpenEdxLmsAPITemplate( |             return new OpenEdxLmsAPITemplate( | ||||||
|                     lmsSetup, |  | ||||||
|                     openEdxCourseAccess, |                     openEdxCourseAccess, | ||||||
|                     openEdxCourseRestriction); |                     openEdxCourseRestriction); | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|  | @ -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.model.institution.LmsSetupTestResult; | ||||||
| import ch.ethz.seb.sebserver.gbl.util.Result; | import ch.ethz.seb.sebserver.gbl.util.Result; | ||||||
| import ch.ethz.seb.sebserver.gbl.util.Utils; | import ch.ethz.seb.sebserver.gbl.util.Utils; | ||||||
|  | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier; | ||||||
| 
 | 
 | ||||||
| final class OpenEdxRestTemplateFactory { | final class OpenEdxRestTemplateFactory { | ||||||
| 
 | 
 | ||||||
|     private static final String OPEN_EDX_DEFAULT_TOKEN_REQUEST_PATH = "/oauth2/access_token"; |     private static final String OPEN_EDX_DEFAULT_TOKEN_REQUEST_PATH = "/oauth2/access_token"; | ||||||
| 
 | 
 | ||||||
|     final LmsSetup lmsSetup; |     final APITemplateDataSupplier apiTemplateDataSupplier; | ||||||
|     final ClientCredentials credentials; |  | ||||||
|     final ProxyData proxyData; |  | ||||||
|     final ClientHttpRequestFactoryService clientHttpRequestFactoryService; |     final ClientHttpRequestFactoryService clientHttpRequestFactoryService; | ||||||
|     final ClientCredentialService clientCredentialService; |     final ClientCredentialService clientCredentialService; | ||||||
|     final Set<String> knownTokenAccessPaths; |     final Set<String> knownTokenAccessPaths; | ||||||
| 
 | 
 | ||||||
|     OpenEdxRestTemplateFactory( |     OpenEdxRestTemplateFactory( | ||||||
|             final LmsSetup lmsSetup, |             final APITemplateDataSupplier apiTemplateDataSupplier, | ||||||
|             final ClientCredentials credentials, |  | ||||||
|             final ProxyData proxyData, |  | ||||||
|             final ClientCredentialService clientCredentialService, |             final ClientCredentialService clientCredentialService, | ||||||
|             final ClientHttpRequestFactoryService clientHttpRequestFactoryService, |             final ClientHttpRequestFactoryService clientHttpRequestFactoryService, | ||||||
|             final String[] alternativeTokenRequestPaths) { |             final String[] alternativeTokenRequestPaths) { | ||||||
| 
 | 
 | ||||||
|         this.lmsSetup = lmsSetup; |         this.apiTemplateDataSupplier = apiTemplateDataSupplier; | ||||||
|         this.clientCredentialService = clientCredentialService; |         this.clientCredentialService = clientCredentialService; | ||||||
|         this.credentials = credentials; |  | ||||||
|         this.proxyData = proxyData; |  | ||||||
|         this.clientHttpRequestFactoryService = clientHttpRequestFactoryService; |         this.clientHttpRequestFactoryService = clientHttpRequestFactoryService; | ||||||
| 
 | 
 | ||||||
|         this.knownTokenAccessPaths = new HashSet<>(); |         this.knownTokenAccessPaths = new HashSet<>(); | ||||||
|  | @ -76,26 +71,34 @@ final class OpenEdxRestTemplateFactory { | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     APITemplateDataSupplier getApiTemplateDataSupplier() { | ||||||
|  |         return this.apiTemplateDataSupplier; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     public LmsSetupTestResult test() { |     public LmsSetupTestResult test() { | ||||||
|  | 
 | ||||||
|  |         final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup(); | ||||||
|  |         final ClientCredentials lmsClientCredentials = this.apiTemplateDataSupplier.getLmsClientCredentials(); | ||||||
|  | 
 | ||||||
|         final List<APIMessage> missingAttrs = new ArrayList<>(); |         final List<APIMessage> missingAttrs = new ArrayList<>(); | ||||||
|         if (StringUtils.isBlank(this.lmsSetup.lmsApiUrl)) { |         if (StringUtils.isBlank(lmsSetup.lmsApiUrl)) { | ||||||
|             missingAttrs.add(APIMessage.fieldValidationError( |             missingAttrs.add(APIMessage.fieldValidationError( | ||||||
|                     LMS_SETUP.ATTR_LMS_URL, |                     LMS_SETUP.ATTR_LMS_URL, | ||||||
|                     "lmsSetup:lmsUrl:notNull")); |                     "lmsSetup:lmsUrl:notNull")); | ||||||
|         } else { |         } else { | ||||||
|             // try to connect to the url |             // try to connect to the url | ||||||
|             if (!Utils.pingHost(this.lmsSetup.lmsApiUrl)) { |             if (!Utils.pingHost(lmsSetup.lmsApiUrl)) { | ||||||
|                 missingAttrs.add(APIMessage.fieldValidationError( |                 missingAttrs.add(APIMessage.fieldValidationError( | ||||||
|                         LMS_SETUP.ATTR_LMS_URL, |                         LMS_SETUP.ATTR_LMS_URL, | ||||||
|                         "lmsSetup:lmsUrl:url.invalid")); |                         "lmsSetup:lmsUrl:url.invalid")); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         if (!this.credentials.hasClientId()) { |         if (!lmsClientCredentials.hasClientId()) { | ||||||
|             missingAttrs.add(APIMessage.fieldValidationError( |             missingAttrs.add(APIMessage.fieldValidationError( | ||||||
|                     LMS_SETUP.ATTR_LMS_CLIENTNAME, |                     LMS_SETUP.ATTR_LMS_CLIENTNAME, | ||||||
|                     "lmsSetup:lmsClientname:notNull")); |                     "lmsSetup:lmsClientname:notNull")); | ||||||
|         } |         } | ||||||
|         if (!this.credentials.hasSecret()) { |         if (!lmsClientCredentials.hasSecret()) { | ||||||
|             missingAttrs.add(APIMessage.fieldValidationError( |             missingAttrs.add(APIMessage.fieldValidationError( | ||||||
|                     LMS_SETUP.ATTR_LMS_CLIENTSECRET, |                     LMS_SETUP.ATTR_LMS_CLIENTSECRET, | ||||||
|                     "lmsSetup:lmsClientsecret:notNull")); |                     "lmsSetup:lmsClientsecret:notNull")); | ||||||
|  | @ -120,10 +123,7 @@ final class OpenEdxRestTemplateFactory { | ||||||
| 
 | 
 | ||||||
|     Result<OAuth2RestTemplate> createOAuthRestTemplate(final String accessTokenPath) { |     Result<OAuth2RestTemplate> createOAuthRestTemplate(final String accessTokenPath) { | ||||||
|         return Result.tryCatch(() -> { |         return Result.tryCatch(() -> { | ||||||
|             final OAuth2RestTemplate template = createRestTemplate( |             final OAuth2RestTemplate template = createRestTemplate(accessTokenPath); | ||||||
|                     this.lmsSetup, |  | ||||||
|                     this.credentials, |  | ||||||
|                     accessTokenPath); |  | ||||||
| 
 | 
 | ||||||
|             final OAuth2AccessToken accessToken = template.getAccessToken(); |             final OAuth2AccessToken accessToken = template.getAccessToken(); | ||||||
|             if (accessToken == null) { |             if (accessToken == null) { | ||||||
|  | @ -134,10 +134,11 @@ final class OpenEdxRestTemplateFactory { | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private OAuth2RestTemplate createRestTemplate( |     private OAuth2RestTemplate createRestTemplate(final String accessTokenRequestPath) throws URISyntaxException { | ||||||
|             final LmsSetup lmsSetup, | 
 | ||||||
|             final ClientCredentials credentials, |         final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup(); | ||||||
|             final String accessTokenRequestPath) throws URISyntaxException { |         final ClientCredentials credentials = this.apiTemplateDataSupplier.getLmsClientCredentials(); | ||||||
|  |         final ProxyData proxyData = this.apiTemplateDataSupplier.getProxyData(); | ||||||
| 
 | 
 | ||||||
|         final CharSequence plainClientId = credentials.clientId; |         final CharSequence plainClientId = credentials.clientId; | ||||||
|         final CharSequence plainClientSecret = this.clientCredentialService |         final CharSequence plainClientSecret = this.clientCredentialService | ||||||
|  | @ -150,7 +151,7 @@ final class OpenEdxRestTemplateFactory { | ||||||
|         details.setClientSecret(plainClientSecret.toString()); |         details.setClientSecret(plainClientSecret.toString()); | ||||||
| 
 | 
 | ||||||
|         final ClientHttpRequestFactory clientHttpRequestFactory = this.clientHttpRequestFactoryService |         final ClientHttpRequestFactory clientHttpRequestFactory = this.clientHttpRequestFactoryService | ||||||
|                 .getClientHttpRequestFactory(this.proxyData) |                 .getClientHttpRequestFactory(proxyData) | ||||||
|                 .getOrThrow(); |                 .getOrThrow(); | ||||||
| 
 | 
 | ||||||
|         final OAuth2RestTemplate template = new OAuth2RestTemplate(details); |         final OAuth2RestTemplate template = new OAuth2RestTemplate(details); | ||||||
|  |  | ||||||
|  | @ -6,18 +6,16 @@ | ||||||
|  * file, You can obtain one at http://mozilla.org/MPL/2.0/. |  * file, You can obtain one at http://mozilla.org/MPL/2.0/. | ||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
| package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl; | package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.mockup; | ||||||
| 
 | 
 | ||||||
| import org.springframework.context.annotation.Lazy; | import org.springframework.context.annotation.Lazy; | ||||||
| import org.springframework.stereotype.Service; | 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.model.institution.LmsSetup.LmsType; | ||||||
| import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; | import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; | ||||||
| import ch.ethz.seb.sebserver.gbl.util.Result; | import ch.ethz.seb.sebserver.gbl.util.Result; | ||||||
| import ch.ethz.seb.sebserver.webservice.WebserviceInfo; | 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.LmsAPITemplate; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplateFactory; | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplateFactory; | ||||||
| 
 | 
 | ||||||
|  | @ -38,14 +36,9 @@ public class MockLmsAPITemplateFactory implements LmsAPITemplateFactory { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public Result<LmsAPITemplate> create( |     public Result<LmsAPITemplate> create(final APITemplateDataSupplier apiTemplateDataSupplier) { | ||||||
|             final LmsSetup lmsSetup, |  | ||||||
|             final ClientCredentials credentials, |  | ||||||
|             final ProxyData proxyData) { |  | ||||||
| 
 |  | ||||||
|         return Result.tryCatch(() -> new MockupLmsAPITemplate( |         return Result.tryCatch(() -> new MockupLmsAPITemplate( | ||||||
|                 lmsSetup, |                 apiTemplateDataSupplier, | ||||||
|                 credentials, |  | ||||||
|                 this.webserviceInfo)); |                 this.webserviceInfo)); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -6,7 +6,7 @@ | ||||||
|  * file, You can obtain one at http://mozilla.org/MPL/2.0/. |  * file, You can obtain one at http://mozilla.org/MPL/2.0/. | ||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
| package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl; | package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.mockup; | ||||||
| 
 | 
 | ||||||
| import java.util.ArrayList; | import java.util.ArrayList; | ||||||
| import java.util.Collection; | import java.util.Collection; | ||||||
|  | @ -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.gbl.util.Result; | ||||||
| import ch.ethz.seb.sebserver.webservice.WebserviceInfo; | import ch.ethz.seb.sebserver.webservice.WebserviceInfo; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; | import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; | ||||||
|  | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService; | 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.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 static final Logger log = LoggerFactory.getLogger(MockupLmsAPITemplate.class); | ||||||
| 
 | 
 | ||||||
|     private final LmsSetup lmsSetup; |  | ||||||
|     private final ClientCredentials credentials; |  | ||||||
|     private final Collection<QuizData> mockups; |     private final Collection<QuizData> mockups; | ||||||
|     private final WebserviceInfo webserviceInfo; |     private final WebserviceInfo webserviceInfo; | ||||||
|  |     private final APITemplateDataSupplier apiTemplateDataSupplier; | ||||||
| 
 | 
 | ||||||
|     MockupLmsAPITemplate( |     MockupLmsAPITemplate( | ||||||
|             final LmsSetup lmsSetup, |             final APITemplateDataSupplier apiTemplateDataSupplier, | ||||||
|             final ClientCredentials credentials, |  | ||||||
|             final WebserviceInfo webserviceInfo) { |             final WebserviceInfo webserviceInfo) { | ||||||
| 
 | 
 | ||||||
|         this.lmsSetup = lmsSetup; |         this.apiTemplateDataSupplier = apiTemplateDataSupplier; | ||||||
|         this.credentials = credentials; |  | ||||||
|         this.webserviceInfo = webserviceInfo; |         this.webserviceInfo = webserviceInfo; | ||||||
|  |         this.mockups = new ArrayList<>(); | ||||||
| 
 | 
 | ||||||
|  |         final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup(); | ||||||
|         final Long lmsSetupId = lmsSetup.id; |         final Long lmsSetupId = lmsSetup.id; | ||||||
|         final Long institutionId = lmsSetup.getInstitutionId(); |         final Long institutionId = lmsSetup.getInstitutionId(); | ||||||
|         final LmsType lmsType = lmsSetup.getLmsType(); |         final LmsType lmsType = lmsSetup.getLmsType(); | ||||||
|         this.mockups = new ArrayList<>(); | 
 | ||||||
|         this.mockups.add(new QuizData( |         this.mockups.add(new QuizData( | ||||||
|                 "quiz1", institutionId, lmsSetupId, lmsType, "Demo Quiz 1 (MOCKUP)", "<p>Demo Quiz Mockup</p>", |                 "quiz1", institutionId, lmsSetupId, lmsType, "Demo Quiz 1 (MOCKUP)", "<p>Demo Quiz Mockup</p>", | ||||||
|                 "2020-01-01T09:00:00Z", null, "http://lms.mockup.com/api/")); |                 "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/")); |                 "http://lms.mockup.com/api/")); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     @Override | ||||||
|  |     public LmsType getType() { | ||||||
|  |         return LmsType.MOCKUP; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     @Override |     @Override | ||||||
|     public LmsSetup lmsSetup() { |     public LmsSetup lmsSetup() { | ||||||
|         return this.lmsSetup; |         return this.apiTemplateDataSupplier.getLmsSetup(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private List<APIMessage> checkAttributes() { |     private List<APIMessage> checkAttributes() { | ||||||
|  |         final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup(); | ||||||
|  |         final ClientCredentials lmsClientCredentials = this.apiTemplateDataSupplier.getLmsClientCredentials(); | ||||||
|         final List<APIMessage> missingAttrs = new ArrayList<>(); |         final List<APIMessage> missingAttrs = new ArrayList<>(); | ||||||
|         if (StringUtils.isBlank(this.lmsSetup.lmsApiUrl)) { |         if (StringUtils.isBlank(lmsSetup.lmsApiUrl)) { | ||||||
|             missingAttrs.add(APIMessage.fieldValidationError( |             missingAttrs.add(APIMessage.fieldValidationError( | ||||||
|                     LMS_SETUP.ATTR_LMS_URL, |                     LMS_SETUP.ATTR_LMS_URL, | ||||||
|                     "lmsSetup:lmsUrl:notNull")); |                     "lmsSetup:lmsUrl:notNull")); | ||||||
|         } |         } | ||||||
|         if (!this.credentials.hasClientId()) { |         if (!lmsClientCredentials.hasClientId()) { | ||||||
|             missingAttrs.add(APIMessage.fieldValidationError( |             missingAttrs.add(APIMessage.fieldValidationError( | ||||||
|                     LMS_SETUP.ATTR_LMS_CLIENTNAME, |                     LMS_SETUP.ATTR_LMS_CLIENTNAME, | ||||||
|                     "lmsSetup:lmsClientname:notNull")); |                     "lmsSetup:lmsClientname:notNull")); | ||||||
|         } |         } | ||||||
|         if (!this.credentials.hasSecret()) { |         if (!lmsClientCredentials.hasSecret()) { | ||||||
|             missingAttrs.add(APIMessage.fieldValidationError( |             missingAttrs.add(APIMessage.fieldValidationError( | ||||||
|                     LMS_SETUP.ATTR_LMS_CLIENTSECRET, |                     LMS_SETUP.ATTR_LMS_CLIENTSECRET, | ||||||
|                     "lmsSetup:lmsClientsecret:notNull")); |                     "lmsSetup:lmsClientsecret:notNull")); | ||||||
|  | @ -119,7 +127,7 @@ final class MockupLmsAPITemplate implements LmsAPITemplate { | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public LmsSetupTestResult testCourseAccessAPI() { |     public LmsSetupTestResult testCourseAccessAPI() { | ||||||
|         log.info("Test Lms Binding for Mockup and LmsSetup: {}", this.lmsSetup); |         log.info("Test Lms Binding for Mockup and LmsSetup: {}", this.apiTemplateDataSupplier.getLmsSetup()); | ||||||
| 
 | 
 | ||||||
|         final List<APIMessage> missingAttrs = checkAttributes(); |         final List<APIMessage> missingAttrs = checkAttributes(); | ||||||
| 
 | 
 | ||||||
|  | @ -136,7 +144,6 @@ final class MockupLmsAPITemplate implements LmsAPITemplate { | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public LmsSetupTestResult testCourseRestrictionAPI() { |     public LmsSetupTestResult testCourseRestrictionAPI() { | ||||||
|         // TODO Auto-generated method stub |  | ||||||
|         return LmsSetupTestResult.ofQuizRestrictionAPIError("unsupported"); |         return LmsSetupTestResult.ofQuizRestrictionAPIError("unsupported"); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -239,7 +246,7 @@ final class MockupLmsAPITemplate implements LmsAPITemplate { | ||||||
|     private boolean authenticate() { |     private boolean authenticate() { | ||||||
|         try { |         try { | ||||||
| 
 | 
 | ||||||
|             final CharSequence plainClientId = this.credentials.clientId; |             final CharSequence plainClientId = this.apiTemplateDataSupplier.getLmsClientCredentials().clientId; | ||||||
|             if (plainClientId == null || plainClientId.length() <= 0) { |             if (plainClientId == null || plainClientId.length() <= 0) { | ||||||
|                 throw new IllegalAccessException("Wrong client credential"); |                 throw new IllegalAccessException("Wrong client credential"); | ||||||
|             } |             } | ||||||
|  | @ -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.Result; | ||||||
| import ch.ethz.seb.sebserver.gbl.util.Utils; | import ch.ethz.seb.sebserver.gbl.util.Utils; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; | import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.lms.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.MoodleCourseDataAsyncLoader.CourseDataShort; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory.MoodleAPIRestTemplate; | 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. |  * background task if needed and return immediately to do not block the request. | ||||||
|  * The planed Moodle integration on moodle side also defines an improved course access API. This will |  * The planed Moodle integration on moodle side also defines an improved course access API. This will | ||||||
|  * possibly make this synchronous fetch strategy obsolete in the future. */ |  * possibly make this synchronous fetch strategy obsolete in the future. */ | ||||||
| public class MoodleCourseAccess extends CourseAccess { | public class MoodleCourseAccess extends AbstractCourseAccess { | ||||||
| 
 | 
 | ||||||
|     private static final long INITIAL_WAIT_TIME = 3 * Constants.SECOND_IN_MILLIS; |     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"; |     static final String MOODLE_COURSE_API_SEARCH_PAGE_SIZE = "perpage"; | ||||||
| 
 | 
 | ||||||
|     private final JSONMapper jsonMapper; |     private final JSONMapper jsonMapper; | ||||||
|     private final LmsSetup lmsSetup; |  | ||||||
|     private final MoodleRestTemplateFactory moodleRestTemplateFactory; |     private final MoodleRestTemplateFactory moodleRestTemplateFactory; | ||||||
|     private final MoodleCourseDataAsyncLoader moodleCourseDataAsyncLoader; |     private final MoodleCourseDataAsyncLoader moodleCourseDataAsyncLoader; | ||||||
|     private final CircuitBreaker<List<QuizData>> allQuizzesRequest; |     private final CircuitBreaker<List<QuizData>> allQuizzesRequest; | ||||||
|  | @ -96,7 +96,6 @@ public class MoodleCourseAccess extends CourseAccess { | ||||||
| 
 | 
 | ||||||
|     protected MoodleCourseAccess( |     protected MoodleCourseAccess( | ||||||
|             final JSONMapper jsonMapper, |             final JSONMapper jsonMapper, | ||||||
|             final LmsSetup lmsSetup, |  | ||||||
|             final MoodleRestTemplateFactory moodleRestTemplateFactory, |             final MoodleRestTemplateFactory moodleRestTemplateFactory, | ||||||
|             final MoodleCourseDataAsyncLoader moodleCourseDataAsyncLoader, |             final MoodleCourseDataAsyncLoader moodleCourseDataAsyncLoader, | ||||||
|             final AsyncService asyncService, |             final AsyncService asyncService, | ||||||
|  | @ -104,7 +103,6 @@ public class MoodleCourseAccess extends CourseAccess { | ||||||
| 
 | 
 | ||||||
|         super(asyncService, environment); |         super(asyncService, environment); | ||||||
|         this.jsonMapper = jsonMapper; |         this.jsonMapper = jsonMapper; | ||||||
|         this.lmsSetup = lmsSetup; |  | ||||||
|         this.moodleCourseDataAsyncLoader = moodleCourseDataAsyncLoader; |         this.moodleCourseDataAsyncLoader = moodleCourseDataAsyncLoader; | ||||||
|         this.moodleRestTemplateFactory = moodleRestTemplateFactory; |         this.moodleRestTemplateFactory = moodleRestTemplateFactory; | ||||||
| 
 | 
 | ||||||
|  | @ -136,6 +134,10 @@ public class MoodleCourseAccess extends CourseAccess { | ||||||
|         }; |         }; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     APITemplateDataSupplier getApiTemplateDataSupplier() { | ||||||
|  |         return this.moodleRestTemplateFactory.apiTemplateDataSupplier; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     @Override |     @Override | ||||||
|     public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeSessionId) { |     public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeSessionId) { | ||||||
|         return Result.tryCatch(() -> { |         return Result.tryCatch(() -> { | ||||||
|  | @ -151,8 +153,9 @@ public class MoodleCourseAccess extends CourseAccess { | ||||||
|                     queryAttributes); |                     queryAttributes); | ||||||
| 
 | 
 | ||||||
|             if (checkAccessDeniedError(userDetailsJSON)) { |             if (checkAccessDeniedError(userDetailsJSON)) { | ||||||
|  |                 final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup(); | ||||||
|                 log.error("Get access denied error from Moodle: {} for API call: {}, response: {}", |                 log.error("Get access denied error from Moodle: {} for API call: {}, response: {}", | ||||||
|                         this.lmsSetup, |                         lmsSetup, | ||||||
|                         MOODLE_USER_PROFILE_API_FUNCTION_NAME, |                         MOODLE_USER_PROFILE_API_FUNCTION_NAME, | ||||||
|                         Utils.truncateText(userDetailsJSON, 2000)); |                         Utils.truncateText(userDetailsJSON, 2000)); | ||||||
|                 throw new RuntimeException("No user details on Moodle API request (access-denied)"); |                 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 MoodleAPIRestTemplate restTemplate, | ||||||
|             final FilterMap filterMap) { |             final FilterMap filterMap) { | ||||||
| 
 | 
 | ||||||
|         final String urlPrefix = (this.lmsSetup.lmsApiUrl.endsWith(Constants.URL_PATH_SEPARATOR)) |         final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup(); | ||||||
|                 ? 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; | ||||||
| 
 | 
 | ||||||
|         final DateTime quizFromTime = (filterMap != null) ? filterMap.getQuizFromTime() : null; |         final DateTime quizFromTime = (filterMap != null) ? filterMap.getQuizFromTime() : null; | ||||||
|         final long fromCutTime = (quizFromTime != null) ? Utils.toUnixTimeInSeconds(quizFromTime) : -1; |         final long fromCutTime = (quizFromTime != null) ? Utils.toUnixTimeInSeconds(quizFromTime) : -1; | ||||||
|  | @ -313,10 +318,10 @@ public class MoodleCourseAccess extends CourseAccess { | ||||||
| 
 | 
 | ||||||
|     private List<QuizData> getCached() { |     private List<QuizData> getCached() { | ||||||
|         final Collection<CourseDataShort> courseQuizData = this.moodleCourseDataAsyncLoader.getCachedCourseData(); |         final Collection<CourseDataShort> courseQuizData = this.moodleCourseDataAsyncLoader.getCachedCourseData(); | ||||||
| 
 |         final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup(); | ||||||
|         final String urlPrefix = (this.lmsSetup.lmsApiUrl.endsWith(Constants.URL_PATH_SEPARATOR)) |         final String urlPrefix = (lmsSetup.lmsApiUrl.endsWith(Constants.URL_PATH_SEPARATOR)) | ||||||
|                 ? this.lmsSetup.lmsApiUrl + MOODLE_QUIZ_START_URL_PATH |                 ? lmsSetup.lmsApiUrl + MOODLE_QUIZ_START_URL_PATH | ||||||
|                 : this.lmsSetup.lmsApiUrl + Constants.URL_PATH_SEPARATOR + MOODLE_QUIZ_START_URL_PATH; |                 : lmsSetup.lmsApiUrl + Constants.URL_PATH_SEPARATOR + MOODLE_QUIZ_START_URL_PATH; | ||||||
| 
 | 
 | ||||||
|         return reduceCoursesToQuizzes(urlPrefix, courseQuizData); |         return reduceCoursesToQuizzes(urlPrefix, courseQuizData); | ||||||
|     } |     } | ||||||
|  | @ -325,13 +330,14 @@ public class MoodleCourseAccess extends CourseAccess { | ||||||
|             final String urlPrefix, |             final String urlPrefix, | ||||||
|             final Collection<CourseDataShort> courseQuizData) { |             final Collection<CourseDataShort> courseQuizData) { | ||||||
| 
 | 
 | ||||||
|  |         final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup(); | ||||||
|         return courseQuizData |         return courseQuizData | ||||||
|                 .stream() |                 .stream() | ||||||
|                 .reduce( |                 .reduce( | ||||||
|                         new ArrayList<>(), |                         new ArrayList<>(), | ||||||
|                         (list, courseData) -> { |                         (list, courseData) -> { | ||||||
|                             list.addAll(quizDataOf( |                             list.addAll(quizDataOf( | ||||||
|                                     this.lmsSetup, |                                     lmsSetup, | ||||||
|                                     courseData, |                                     courseData, | ||||||
|                                     urlPrefix)); |                                     urlPrefix)); | ||||||
|                             return list; |                             return list; | ||||||
|  | @ -380,16 +386,17 @@ public class MoodleCourseAccess extends CourseAccess { | ||||||
|             final CourseQuizData courseQuizData = this.jsonMapper.readValue( |             final CourseQuizData courseQuizData = this.jsonMapper.readValue( | ||||||
|                     quizzesJSON, |                     quizzesJSON, | ||||||
|                     CourseQuizData.class); |                     CourseQuizData.class); | ||||||
|  |             final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup(); | ||||||
| 
 | 
 | ||||||
|             if (courseQuizData == null) { |             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(); |                 return Collections.emptyList(); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             logMoodleWarnings(courseQuizData.warnings); |             logMoodleWarnings(courseQuizData.warnings); | ||||||
| 
 | 
 | ||||||
|             if (courseQuizData.quizzes == null || courseQuizData.quizzes.isEmpty()) { |             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(); |                 return Collections.emptyList(); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|  | @ -402,9 +409,9 @@ public class MoodleCourseAccess extends CourseAccess { | ||||||
|                         } |                         } | ||||||
|                     }); |                     }); | ||||||
| 
 | 
 | ||||||
|             final String urlPrefix = (this.lmsSetup.lmsApiUrl.endsWith(Constants.URL_PATH_SEPARATOR)) |             final String urlPrefix = (lmsSetup.lmsApiUrl.endsWith(Constants.URL_PATH_SEPARATOR)) | ||||||
|                     ? this.lmsSetup.lmsApiUrl + MOODLE_QUIZ_START_URL_PATH |                     ? lmsSetup.lmsApiUrl + MOODLE_QUIZ_START_URL_PATH | ||||||
|                     : this.lmsSetup.lmsApiUrl + Constants.URL_PATH_SEPARATOR + MOODLE_QUIZ_START_URL_PATH; |                     : lmsSetup.lmsApiUrl + Constants.URL_PATH_SEPARATOR + MOODLE_QUIZ_START_URL_PATH; | ||||||
| 
 | 
 | ||||||
|             return courseData.values() |             return courseData.values() | ||||||
|                     .stream() |                     .stream() | ||||||
|  | @ -413,7 +420,7 @@ public class MoodleCourseAccess extends CourseAccess { | ||||||
|                             new ArrayList<>(), |                             new ArrayList<>(), | ||||||
|                             (list, cd) -> { |                             (list, cd) -> { | ||||||
|                                 list.addAll(quizDataOf( |                                 list.addAll(quizDataOf( | ||||||
|                                         this.lmsSetup, |                                         lmsSetup, | ||||||
|                                         cd, |                                         cd, | ||||||
|                                         urlPrefix)); |                                         urlPrefix)); | ||||||
|                                 return list; |                                 return list; | ||||||
|  | @ -451,16 +458,17 @@ public class MoodleCourseAccess extends CourseAccess { | ||||||
|             final Courses courses = this.jsonMapper.readValue( |             final Courses courses = this.jsonMapper.readValue( | ||||||
|                     coursePageJSON, |                     coursePageJSON, | ||||||
|                     Courses.class); |                     Courses.class); | ||||||
|  |             final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup(); | ||||||
| 
 | 
 | ||||||
|             if (courses == null) { |             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(); |                 return Collections.emptyList(); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             logMoodleWarnings(courses.warnings); |             logMoodleWarnings(courses.warnings); | ||||||
| 
 | 
 | ||||||
|             if (courses.courses == null || courses.courses.isEmpty()) { |             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(); |                 return Collections.emptyList(); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|  | @ -630,9 +638,10 @@ public class MoodleCourseAccess extends CourseAccess { | ||||||
|     private void logMoodleWarnings(final Collection<Warning> warnings) { |     private void logMoodleWarnings(final Collection<Warning> warnings) { | ||||||
|         if (warnings != null && !warnings.isEmpty()) { |         if (warnings != null && !warnings.isEmpty()) { | ||||||
|             if (log.isDebugEnabled()) { |             if (log.isDebugEnabled()) { | ||||||
|  |                 final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup(); | ||||||
|                 log.debug( |                 log.debug( | ||||||
|                         "There are warnings from Moodle response: Moodle: {} request: {} warnings: {} warning sample: {}", |                         "There are warnings from Moodle response: Moodle: {} request: {} warnings: {} warning sample: {}", | ||||||
|                         this.lmsSetup, |                         lmsSetup, | ||||||
|                         MoodleCourseAccess.MOODLE_QUIZ_API_FUNCTION_NAME, |                         MoodleCourseAccess.MOODLE_QUIZ_API_FUNCTION_NAME, | ||||||
|                         warnings.size(), |                         warnings.size(), | ||||||
|                         warnings.iterator().next().toString()); |                         warnings.iterator().next().toString()); | ||||||
|  |  | ||||||
|  | @ -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.QuizData; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction; | import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; | import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; | ||||||
|  | import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult; | import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails; | import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails; | ||||||
| import ch.ethz.seb.sebserver.gbl.util.Result; | 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 static final Logger log = LoggerFactory.getLogger(MoodleLmsAPITemplate.class); | ||||||
| 
 | 
 | ||||||
|     private final LmsSetup lmsSetup; |  | ||||||
|     private final MoodleCourseAccess moodleCourseAccess; |     private final MoodleCourseAccess moodleCourseAccess; | ||||||
|     private final MoodleCourseRestriction moodleCourseRestriction; |     private final MoodleCourseRestriction moodleCourseRestriction; | ||||||
| 
 | 
 | ||||||
|     protected MoodleLmsAPITemplate( |     protected MoodleLmsAPITemplate( | ||||||
|             final LmsSetup lmsSetup, |  | ||||||
|             final MoodleCourseAccess moodleCourseAccess, |             final MoodleCourseAccess moodleCourseAccess, | ||||||
|             final MoodleCourseRestriction moodleCourseRestriction) { |             final MoodleCourseRestriction moodleCourseRestriction) { | ||||||
| 
 | 
 | ||||||
|         this.lmsSetup = lmsSetup; |  | ||||||
|         this.moodleCourseAccess = moodleCourseAccess; |         this.moodleCourseAccess = moodleCourseAccess; | ||||||
|         this.moodleCourseRestriction = moodleCourseRestriction; |         this.moodleCourseRestriction = moodleCourseRestriction; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     @Override | ||||||
|  |     public LmsType getType() { | ||||||
|  |         return LmsType.MOODLE; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     @Override |     @Override | ||||||
|     public LmsSetup lmsSetup() { |     public LmsSetup lmsSetup() { | ||||||
|         return this.lmsSetup; |         return this.moodleCourseAccess | ||||||
|  |                 .getApiTemplateDataSupplier() | ||||||
|  |                 .getLmsSetup(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|  |  | ||||||
|  | @ -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.api.JSONMapper; | ||||||
| import ch.ethz.seb.sebserver.gbl.async.AsyncService; | import ch.ethz.seb.sebserver.gbl.async.AsyncService; | ||||||
| import ch.ethz.seb.sebserver.gbl.client.ClientCredentialService; | import ch.ethz.seb.sebserver.gbl.client.ClientCredentialService; | ||||||
| import ch.ethz.seb.sebserver.gbl.client.ClientCredentials; |  | ||||||
| import ch.ethz.seb.sebserver.gbl.client.ProxyData; |  | ||||||
| import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; | import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType; | import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType; | ||||||
| import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; | import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; | ||||||
| import ch.ethz.seb.sebserver.gbl.util.Result; | 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.LmsAPITemplate; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplateFactory; | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplateFactory; | ||||||
| 
 | 
 | ||||||
|  | @ -68,29 +67,25 @@ public class MoodleLmsAPITemplateFactory implements LmsAPITemplateFactory { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public Result<LmsAPITemplate> create( |     public Result<LmsAPITemplate> create(final APITemplateDataSupplier apiTemplateDataSupplier) { | ||||||
|             final LmsSetup lmsSetup, |  | ||||||
|             final ClientCredentials credentials, |  | ||||||
|             final ProxyData proxyData) { |  | ||||||
| 
 | 
 | ||||||
|         return Result.tryCatch(() -> { |         return Result.tryCatch(() -> { | ||||||
| 
 | 
 | ||||||
|             final MoodleCourseDataAsyncLoader asyncLoaderPrototype = |             final LmsSetup lmsSetup = apiTemplateDataSupplier.getLmsSetup(); | ||||||
|                     this.applicationContext.getBean(MoodleCourseDataAsyncLoader.class); | 
 | ||||||
|             asyncLoaderPrototype.init(lmsSetup.name); |             final MoodleCourseDataAsyncLoader asyncLoaderPrototype = this.applicationContext | ||||||
|  |                     .getBean(MoodleCourseDataAsyncLoader.class); | ||||||
|  |             asyncLoaderPrototype.init(lmsSetup.getModelId()); | ||||||
| 
 | 
 | ||||||
|             final MoodleRestTemplateFactory moodleRestTemplateFactory = new MoodleRestTemplateFactory( |             final MoodleRestTemplateFactory moodleRestTemplateFactory = new MoodleRestTemplateFactory( | ||||||
|                     this.jsonMapper, |                     this.jsonMapper, | ||||||
|                     lmsSetup, |                     apiTemplateDataSupplier, | ||||||
|                     credentials, |  | ||||||
|                     proxyData, |  | ||||||
|                     this.clientCredentialService, |                     this.clientCredentialService, | ||||||
|                     this.clientHttpRequestFactoryService, |                     this.clientHttpRequestFactoryService, | ||||||
|                     this.alternativeTokenRequestPaths); |                     this.alternativeTokenRequestPaths); | ||||||
| 
 | 
 | ||||||
|             final MoodleCourseAccess moodleCourseAccess = new MoodleCourseAccess( |             final MoodleCourseAccess moodleCourseAccess = new MoodleCourseAccess( | ||||||
|                     this.jsonMapper, |                     this.jsonMapper, | ||||||
|                     lmsSetup, |  | ||||||
|                     moodleRestTemplateFactory, |                     moodleRestTemplateFactory, | ||||||
|                     asyncLoaderPrototype, |                     asyncLoaderPrototype, | ||||||
|                     this.asyncService, |                     this.asyncService, | ||||||
|  | @ -101,7 +96,6 @@ public class MoodleLmsAPITemplateFactory implements LmsAPITemplateFactory { | ||||||
|                     moodleRestTemplateFactory); |                     moodleRestTemplateFactory); | ||||||
| 
 | 
 | ||||||
|             return new MoodleLmsAPITemplate( |             return new MoodleLmsAPITemplate( | ||||||
|                     lmsSetup, |  | ||||||
|                     moodleCourseAccess, |                     moodleCourseAccess, | ||||||
|                     moodleCourseRestriction); |                     moodleCourseRestriction); | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|  | @ -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.model.institution.LmsSetupTestResult; | ||||||
| import ch.ethz.seb.sebserver.gbl.util.Result; | import ch.ethz.seb.sebserver.gbl.util.Result; | ||||||
| import ch.ethz.seb.sebserver.gbl.util.Utils; | import ch.ethz.seb.sebserver.gbl.util.Utils; | ||||||
|  | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier; | ||||||
| 
 | 
 | ||||||
| class MoodleRestTemplateFactory { | class MoodleRestTemplateFactory { | ||||||
| 
 | 
 | ||||||
|     private static final Logger log = LoggerFactory.getLogger(MoodleRestTemplateFactory.class); |     private static final Logger log = LoggerFactory.getLogger(MoodleRestTemplateFactory.class); | ||||||
| 
 | 
 | ||||||
|     final JSONMapper jsonMapper; |     final JSONMapper jsonMapper; | ||||||
|     final LmsSetup lmsSetup; |     final APITemplateDataSupplier apiTemplateDataSupplier; | ||||||
|     final ClientCredentials credentials; |  | ||||||
|     final ProxyData proxyData; |  | ||||||
|     final ClientHttpRequestFactoryService clientHttpRequestFactoryService; |     final ClientHttpRequestFactoryService clientHttpRequestFactoryService; | ||||||
|     final ClientCredentialService clientCredentialService; |     final ClientCredentialService clientCredentialService; | ||||||
|     final Set<String> knownTokenAccessPaths; |     final Set<String> knownTokenAccessPaths; | ||||||
| 
 | 
 | ||||||
|     public MoodleRestTemplateFactory( |     public MoodleRestTemplateFactory( | ||||||
|             final JSONMapper jsonMapper, |             final JSONMapper jsonMapper, | ||||||
|             final LmsSetup lmsSetup, |             final APITemplateDataSupplier apiTemplateDataSupplier, | ||||||
|             final ClientCredentials credentials, |  | ||||||
|             final ProxyData proxyData, |  | ||||||
|             final ClientCredentialService clientCredentialService, |             final ClientCredentialService clientCredentialService, | ||||||
|             final ClientHttpRequestFactoryService clientHttpRequestFactoryService, |             final ClientHttpRequestFactoryService clientHttpRequestFactoryService, | ||||||
|             final String[] alternativeTokenRequestPaths) { |             final String[] alternativeTokenRequestPaths) { | ||||||
| 
 | 
 | ||||||
|         this.jsonMapper = jsonMapper; |         this.jsonMapper = jsonMapper; | ||||||
|         this.lmsSetup = lmsSetup; |         this.apiTemplateDataSupplier = apiTemplateDataSupplier; | ||||||
|         this.clientCredentialService = clientCredentialService; |         this.clientCredentialService = clientCredentialService; | ||||||
|         this.credentials = credentials; |  | ||||||
|         this.proxyData = proxyData; |  | ||||||
|         this.clientHttpRequestFactoryService = clientHttpRequestFactoryService; |         this.clientHttpRequestFactoryService = clientHttpRequestFactoryService; | ||||||
| 
 | 
 | ||||||
|         this.knownTokenAccessPaths = new HashSet<>(); |         this.knownTokenAccessPaths = new HashSet<>(); | ||||||
|  | @ -86,28 +81,36 @@ class MoodleRestTemplateFactory { | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     APITemplateDataSupplier getApiTemplateDataSupplier() { | ||||||
|  |         return this.apiTemplateDataSupplier; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     public LmsSetupTestResult test() { |     public LmsSetupTestResult test() { | ||||||
|  | 
 | ||||||
|  |         final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup(); | ||||||
|  |         final ClientCredentials credentials = this.apiTemplateDataSupplier.getLmsClientCredentials(); | ||||||
|  | 
 | ||||||
|         final List<APIMessage> missingAttrs = new ArrayList<>(); |         final List<APIMessage> missingAttrs = new ArrayList<>(); | ||||||
|         if (StringUtils.isBlank(this.lmsSetup.lmsApiUrl)) { |         if (StringUtils.isBlank(lmsSetup.lmsApiUrl)) { | ||||||
|             missingAttrs.add(APIMessage.fieldValidationError( |             missingAttrs.add(APIMessage.fieldValidationError( | ||||||
|                     LMS_SETUP.ATTR_LMS_URL, |                     LMS_SETUP.ATTR_LMS_URL, | ||||||
|                     "lmsSetup:lmsUrl:notNull")); |                     "lmsSetup:lmsUrl:notNull")); | ||||||
|         } else { |         } else { | ||||||
|             // try to connect to the url |             // try to connect to the url | ||||||
|             if (!Utils.pingHost(this.lmsSetup.lmsApiUrl)) { |             if (!Utils.pingHost(lmsSetup.lmsApiUrl)) { | ||||||
|                 missingAttrs.add(APIMessage.fieldValidationError( |                 missingAttrs.add(APIMessage.fieldValidationError( | ||||||
|                         LMS_SETUP.ATTR_LMS_URL, |                         LMS_SETUP.ATTR_LMS_URL, | ||||||
|                         "lmsSetup:lmsUrl:url.invalid")); |                         "lmsSetup:lmsUrl:url.invalid")); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (StringUtils.isBlank(this.lmsSetup.lmsRestApiToken)) { |         if (StringUtils.isBlank(lmsSetup.lmsRestApiToken)) { | ||||||
|             if (!this.credentials.hasClientId()) { |             if (!credentials.hasClientId()) { | ||||||
|                 missingAttrs.add(APIMessage.fieldValidationError( |                 missingAttrs.add(APIMessage.fieldValidationError( | ||||||
|                         LMS_SETUP.ATTR_LMS_CLIENTNAME, |                         LMS_SETUP.ATTR_LMS_CLIENTNAME, | ||||||
|                         "lmsSetup:lmsClientname:notNull")); |                         "lmsSetup:lmsClientname:notNull")); | ||||||
|             } |             } | ||||||
|             if (!this.credentials.hasSecret()) { |             if (!credentials.hasSecret()) { | ||||||
|                 missingAttrs.add(APIMessage.fieldValidationError( |                 missingAttrs.add(APIMessage.fieldValidationError( | ||||||
|                         LMS_SETUP.ATTR_LMS_CLIENTSECRET, |                         LMS_SETUP.ATTR_LMS_CLIENTSECRET, | ||||||
|                         "lmsSetup:lmsClientsecret:notNull")); |                         "lmsSetup:lmsClientsecret:notNull")); | ||||||
|  | @ -122,14 +125,17 @@ class MoodleRestTemplateFactory { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     Result<MoodleAPIRestTemplate> createRestTemplate() { |     Result<MoodleAPIRestTemplate> createRestTemplate() { | ||||||
|  | 
 | ||||||
|  |         final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup(); | ||||||
|  | 
 | ||||||
|         return this.knownTokenAccessPaths |         return this.knownTokenAccessPaths | ||||||
|                 .stream() |                 .stream() | ||||||
|                 .map(this::createRestTemplate) |                 .map(this::createRestTemplate) | ||||||
|                 .map(result -> { |                 .map(result -> { | ||||||
|                     if (result.hasError()) { |                     if (result.hasError()) { | ||||||
|                         log.warn("Failed to get access token for LMS: {}({})", |                         log.warn("Failed to get access token for LMS: {}({})", | ||||||
|                                 this.lmsSetup.name, |                                 lmsSetup.name, | ||||||
|                                 this.lmsSetup.id, |                                 lmsSetup.id, | ||||||
|                                 result.getError()); |                                 result.getError()); | ||||||
|                     } |                     } | ||||||
|                     return result; |                     return result; | ||||||
|  | @ -138,30 +144,17 @@ class MoodleRestTemplateFactory { | ||||||
|                 .findFirst() |                 .findFirst() | ||||||
|                 .orElse(Result.ofRuntimeError( |                 .orElse(Result.ofRuntimeError( | ||||||
|                         "Failed to gain any access for LMS " + |                         "Failed to gain any access for LMS " + | ||||||
|                                 this.lmsSetup.name + "(" + this.lmsSetup.id + |                                 lmsSetup.name + "(" + lmsSetup.id + | ||||||
|                                 ") on paths: " + this.knownTokenAccessPaths)); |                                 ") on paths: " + this.knownTokenAccessPaths)); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     Result<MoodleAPIRestTemplate> createRestTemplate(final String accessTokenPath) { |     Result<MoodleAPIRestTemplate> createRestTemplate(final String accessTokenPath) { | ||||||
|  | 
 | ||||||
|  |         final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup(); | ||||||
|  | 
 | ||||||
|         return Result.tryCatch(() -> { |         return Result.tryCatch(() -> { | ||||||
|             final MoodleAPIRestTemplate template = createRestTemplate( |             final ClientCredentials credentials = this.apiTemplateDataSupplier.getLmsClientCredentials(); | ||||||
|                     this.credentials, |             final ProxyData proxyData = this.apiTemplateDataSupplier.getProxyData(); | ||||||
|                     accessTokenPath); |  | ||||||
| 
 |  | ||||||
|             final CharSequence accessToken = template.getAccessToken(); |  | ||||||
|             if (accessToken == null) { |  | ||||||
|                 throw new RuntimeException("Failed to get access token for LMS " + |  | ||||||
|                         this.lmsSetup.name + "(" + this.lmsSetup.id + |  | ||||||
|                         ") on path: " + accessTokenPath); |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             return template; |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     protected MoodleAPIRestTemplate createRestTemplate( |  | ||||||
|             final ClientCredentials credentials, |  | ||||||
|             final String accessTokenRequestPath) { |  | ||||||
| 
 | 
 | ||||||
|             final CharSequence plainClientId = credentials.clientId; |             final CharSequence plainClientId = credentials.clientId; | ||||||
|             final CharSequence plainClientSecret = this.clientCredentialService |             final CharSequence plainClientSecret = this.clientCredentialService | ||||||
|  | @ -170,19 +163,27 @@ class MoodleRestTemplateFactory { | ||||||
| 
 | 
 | ||||||
|             final MoodleAPIRestTemplate restTemplate = new MoodleAPIRestTemplate( |             final MoodleAPIRestTemplate restTemplate = new MoodleAPIRestTemplate( | ||||||
|                     this.jsonMapper, |                     this.jsonMapper, | ||||||
|                 this.lmsSetup.lmsApiUrl, |                     lmsSetup.lmsApiUrl, | ||||||
|                 accessTokenRequestPath, |                     accessTokenPath, | ||||||
|                 this.lmsSetup.lmsRestApiToken, |                     lmsSetup.lmsRestApiToken, | ||||||
|                     plainClientId, |                     plainClientId, | ||||||
|                     plainClientSecret); |                     plainClientSecret); | ||||||
| 
 | 
 | ||||||
|             final ClientHttpRequestFactory clientHttpRequestFactory = this.clientHttpRequestFactoryService |             final ClientHttpRequestFactory clientHttpRequestFactory = this.clientHttpRequestFactoryService | ||||||
|                 .getClientHttpRequestFactory(this.proxyData) |                     .getClientHttpRequestFactory(proxyData) | ||||||
|                     .getOrThrow(); |                     .getOrThrow(); | ||||||
| 
 | 
 | ||||||
|             restTemplate.setRequestFactory(clientHttpRequestFactory); |             restTemplate.setRequestFactory(clientHttpRequestFactory); | ||||||
|  |             final CharSequence accessToken = restTemplate.getAccessToken(); | ||||||
|  | 
 | ||||||
|  |             if (accessToken == null) { | ||||||
|  |                 throw new RuntimeException("Failed to get access token for LMS " + | ||||||
|  |                         lmsSetup.name + "(" + lmsSetup.id + | ||||||
|  |                         ") on path: " + accessTokenPath); | ||||||
|  |             } | ||||||
| 
 | 
 | ||||||
|             return restTemplate; |             return restTemplate; | ||||||
|  |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public class MoodleAPIRestTemplate extends RestTemplate { |     public class MoodleAPIRestTemplate extends RestTemplate { | ||||||
|  | @ -210,7 +211,6 @@ class MoodleRestTemplateFactory { | ||||||
|         private final HttpEntity<?> tokenReqEntity = new HttpEntity<>(new LinkedMultiValueMap<>()); |         private final HttpEntity<?> tokenReqEntity = new HttpEntity<>(new LinkedMultiValueMap<>()); | ||||||
| 
 | 
 | ||||||
|         protected MoodleAPIRestTemplate( |         protected MoodleAPIRestTemplate( | ||||||
| 
 |  | ||||||
|                 final JSONMapper jsonMapper, |                 final JSONMapper jsonMapper, | ||||||
|                 final String serverURL, |                 final String serverURL, | ||||||
|                 final String tokenPath, |                 final String tokenPath, | ||||||
|  | @ -318,10 +318,13 @@ class MoodleRestTemplateFactory { | ||||||
|                     functionReqEntity, |                     functionReqEntity, | ||||||
|                     String.class); |                     String.class); | ||||||
| 
 | 
 | ||||||
|  |             final LmsSetup lmsSetup = MoodleRestTemplateFactory.this.apiTemplateDataSupplier | ||||||
|  |                     .getLmsSetup(); | ||||||
|  | 
 | ||||||
|             if (response.getStatusCode() != HttpStatus.OK) { |             if (response.getStatusCode() != HttpStatus.OK) { | ||||||
|                 throw new RuntimeException( |                 throw new RuntimeException( | ||||||
|                         "Failed to call Moodle webservice API function: " + functionName + " lms setup: " + |                         "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(); |             final String body = response.getBody(); | ||||||
|  | @ -335,7 +338,7 @@ class MoodleRestTemplateFactory { | ||||||
|                 this.accessToken = null; |                 this.accessToken = null; | ||||||
|                 throw new RuntimeException( |                 throw new RuntimeException( | ||||||
|                         "Failed to call Moodle webservice API function: " + functionName + " lms setup: " + |                         "Failed to call Moodle webservice API function: " + functionName + " lms setup: " + | ||||||
|                                 MoodleRestTemplateFactory.this.lmsSetup + " response: " + body); |                                 lmsSetup + " response: " + body); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             return body; |             return body; | ||||||
|  | @ -343,7 +346,11 @@ class MoodleRestTemplateFactory { | ||||||
| 
 | 
 | ||||||
|         private void requestAccessToken() { |         private void requestAccessToken() { | ||||||
| 
 | 
 | ||||||
|  |             final LmsSetup lmsSetup = MoodleRestTemplateFactory.this.apiTemplateDataSupplier | ||||||
|  |                     .getLmsSetup(); | ||||||
|  | 
 | ||||||
|             try { |             try { | ||||||
|  | 
 | ||||||
|                 final ResponseEntity<String> response = super.exchange( |                 final ResponseEntity<String> response = super.exchange( | ||||||
|                         this.serverURL + this.tokenPath, |                         this.serverURL + this.tokenPath, | ||||||
|                         HttpMethod.GET, |                         HttpMethod.GET, | ||||||
|  | @ -353,11 +360,11 @@ class MoodleRestTemplateFactory { | ||||||
| 
 | 
 | ||||||
|                 if (response.getStatusCode() != HttpStatus.OK) { |                 if (response.getStatusCode() != HttpStatus.OK) { | ||||||
|                     log.error("Failed to gain access token for LMS (Moodle): lmsSetup: {} response: {} : {}", |                     log.error("Failed to gain access token for LMS (Moodle): lmsSetup: {} response: {} : {}", | ||||||
|                             MoodleRestTemplateFactory.this.lmsSetup, |                             lmsSetup, | ||||||
|                             response.getStatusCode(), |                             response.getStatusCode(), | ||||||
|                             response.getBody()); |                             response.getBody()); | ||||||
|                     throw new RuntimeException("Failed to gain access token for LMS (Moodle): lmsSetup: " + |                     throw new RuntimeException("Failed to gain access token for LMS (Moodle): lmsSetup: " + | ||||||
|                             MoodleRestTemplateFactory.this.lmsSetup + " response: " + response.getBody()); |                             lmsSetup + " response: " + response.getBody()); | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 try { |                 try { | ||||||
|  | @ -369,25 +376,25 @@ class MoodleRestTemplateFactory { | ||||||
|                         throw new RuntimeException("Access Token request with 200 but no or invalid token body"); |                         throw new RuntimeException("Access Token request with 200 but no or invalid token body"); | ||||||
|                     } else { |                     } else { | ||||||
|                         log.info("Successfully get access token from Moodle: {}", |                         log.info("Successfully get access token from Moodle: {}", | ||||||
|                                 MoodleRestTemplateFactory.this.lmsSetup); |                                 lmsSetup); | ||||||
|                     } |                     } | ||||||
| 
 | 
 | ||||||
|                     this.accessToken = moodleToken.token; |                     this.accessToken = moodleToken.token; | ||||||
|                 } catch (final Exception e) { |                 } catch (final Exception e) { | ||||||
|                     log.error("Failed to gain access token for LMS (Moodle): lmsSetup: {} response: {} : {}", |                     log.error("Failed to gain access token for LMS (Moodle): lmsSetup: {} response: {} : {}", | ||||||
|                             MoodleRestTemplateFactory.this.lmsSetup, |                             lmsSetup, | ||||||
|                             response.getStatusCode(), |                             response.getStatusCode(), | ||||||
|                             response.getBody()); |                             response.getBody()); | ||||||
|                     throw new RuntimeException("Failed to gain access token for LMS (Moodle): lmsSetup: " + |                     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) { |             } catch (final Exception e) { | ||||||
|                 log.error("Failed to gain access token for LMS (Moodle): lmsSetup: {} :", |                 log.error("Failed to gain access token for LMS (Moodle): lmsSetup: {} :", | ||||||
|                         MoodleRestTemplateFactory.this.lmsSetup, |                         lmsSetup, | ||||||
|                         e); |                         e); | ||||||
|                 throw new RuntimeException("Failed to gain access token for LMS (Moodle): lmsSetup: " + |                 throw new RuntimeException("Failed to gain access token for LMS (Moodle): lmsSetup: " + | ||||||
|                         MoodleRestTemplateFactory.this.lmsSetup + " cause: " + e.getMessage()); |                         lmsSetup + " cause: " + e.getMessage()); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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.api.APIMessage.ErrorMessage; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.exam.Exam; | 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.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.ClientConnection; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData; | import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData; | ||||||
| import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; | 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.FilterMap; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.dao.IndicatorDAO; | 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.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.ExamSessionService; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.indicator.IndicatorDistributedRequestCache; | 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 ExamDAO examDAO; | ||||||
|     private final ExamConfigurationMapDAO examConfigurationMapDAO; |     private final ExamConfigurationMapDAO examConfigurationMapDAO; | ||||||
|     private final CacheManager cacheManager; |     private final CacheManager cacheManager; | ||||||
|     private final LmsAPIService lmsAPIService; |     private final SEBRestrictionService sebRestrictionService; | ||||||
|     private final IndicatorDistributedRequestCache indicatorDistributedRequestCache; |     private final IndicatorDistributedRequestCache indicatorDistributedRequestCache; | ||||||
|     private final boolean distributedSetup; |     private final boolean distributedSetup; | ||||||
| 
 | 
 | ||||||
|  | @ -79,7 +77,7 @@ public class ExamSessionServiceImpl implements ExamSessionService { | ||||||
|             final ClientConnectionDAO clientConnectionDAO, |             final ClientConnectionDAO clientConnectionDAO, | ||||||
|             final IndicatorDAO indicatorDAO, |             final IndicatorDAO indicatorDAO, | ||||||
|             final CacheManager cacheManager, |             final CacheManager cacheManager, | ||||||
|             final LmsAPIService lmsAPIService, |             final SEBRestrictionService sebRestrictionService, | ||||||
|             final IndicatorDistributedRequestCache indicatorDistributedRequestCache, |             final IndicatorDistributedRequestCache indicatorDistributedRequestCache, | ||||||
|             @Value("${sebserver.webservice.distributed:false}") final boolean distributedSetup) { |             @Value("${sebserver.webservice.distributed:false}") final boolean distributedSetup) { | ||||||
| 
 | 
 | ||||||
|  | @ -90,7 +88,7 @@ public class ExamSessionServiceImpl implements ExamSessionService { | ||||||
|         this.clientConnectionDAO = clientConnectionDAO; |         this.clientConnectionDAO = clientConnectionDAO; | ||||||
|         this.cacheManager = cacheManager; |         this.cacheManager = cacheManager; | ||||||
|         this.indicatorDAO = indicatorDAO; |         this.indicatorDAO = indicatorDAO; | ||||||
|         this.lmsAPIService = lmsAPIService; |         this.sebRestrictionService = sebRestrictionService; | ||||||
|         this.indicatorDistributedRequestCache = indicatorDistributedRequestCache; |         this.indicatorDistributedRequestCache = indicatorDistributedRequestCache; | ||||||
|         this.distributedSetup = distributedSetup; |         this.distributedSetup = distributedSetup; | ||||||
|     } |     } | ||||||
|  | @ -117,7 +115,7 @@ public class ExamSessionServiceImpl implements ExamSessionService { | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public LmsAPIService getLmsAPIService() { |     public LmsAPIService getLmsAPIService() { | ||||||
|         return this.lmsAPIService; |         return this.sebRestrictionService.getLmsAPIService(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|  | @ -149,29 +147,10 @@ public class ExamSessionServiceImpl implements ExamSessionService { | ||||||
|                             return null; |                             return null; | ||||||
|                         }); |                         }); | ||||||
| 
 | 
 | ||||||
|                 // check SEB restriction available and restricted |                 if (!this.sebRestrictionService.checkConsistency(exam.lmsSetupId, exam)) { | ||||||
|                 // 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( |                     result.add( | ||||||
|                             ErrorMessage.EXAM_CONSISTENCY_VALIDATION_SEB_RESTRICTION |                             ErrorMessage.EXAM_CONSISTENCY_VALIDATION_SEB_RESTRICTION | ||||||
|                                     .of(exam.getModelId())); |                                     .of(exam.getModelId())); | ||||||
|                                 } else { |  | ||||||
|                                     throw new RuntimeException("Unexpected error: ", error); |  | ||||||
|                                 } |  | ||||||
|                             }); |  | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 // check indicator exists |                 // check indicator exists | ||||||
|  |  | ||||||
|  | @ -246,12 +246,21 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> { | ||||||
|             @RequestParam( |             @RequestParam( | ||||||
|                     name = API.PARAM_INSTITUTION_ID, |                     name = API.PARAM_INSTITUTION_ID, | ||||||
|                     required = true, |                     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); |         checkReadPrivilege(institutionId); | ||||||
|         return this.examSessionService |         final Collection<APIMessage> result = this.examSessionService | ||||||
|                 .checkExamConsistency(modelId) |                 .checkExamConsistency(modelId) | ||||||
|                 .getOrThrow(); |                 .getOrThrow(); | ||||||
|  | 
 | ||||||
|  |         if (includeRestriction) { | ||||||
|  |             // TODO include seb restriction check and status | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return result; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // **************************************************************************** |     // **************************************************************************** | ||||||
|  | @ -524,12 +533,14 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private Result<Exam> applySEBRestriction(final Exam exam, final boolean restrict) { |     private Result<Exam> applySEBRestriction(final Exam exam, final boolean restrict) { | ||||||
|  | 
 | ||||||
|  |         return Result.tryCatch(() -> { | ||||||
|             final LmsSetup lmsSetup = this.lmsAPIService.getLmsSetup(exam.lmsSetupId) |             final LmsSetup lmsSetup = this.lmsAPIService.getLmsSetup(exam.lmsSetupId) | ||||||
|                     .getOrThrow(); |                     .getOrThrow(); | ||||||
| 
 | 
 | ||||||
|             if (!lmsSetup.lmsType.features.contains(Features.SEB_RESTRICTION)) { |             if (!lmsSetup.lmsType.features.contains(Features.SEB_RESTRICTION)) { | ||||||
|             return Result.ofError(new UnsupportedOperationException( |                 throw new UnsupportedOperationException( | ||||||
|                     "SEB Restriction feature not available for LMS type: " + lmsSetup.lmsType)); |                         "SEB Restriction feature not available for LMS type: " + lmsSetup.lmsType); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             if (restrict) { |             if (restrict) { | ||||||
|  | @ -537,24 +548,28 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> { | ||||||
|                         .getLmsSetup(exam.lmsSetupId) |                         .getLmsSetup(exam.lmsSetupId) | ||||||
|                         .getOrThrow().lmsType.features.contains(Features.SEB_RESTRICTION)) { |                         .getOrThrow().lmsType.features.contains(Features.SEB_RESTRICTION)) { | ||||||
| 
 | 
 | ||||||
|                 return Result.ofError(new APIMessageException( |                     throw new APIMessageException( | ||||||
|                             APIMessage.ErrorMessage.ILLEGAL_API_ARGUMENT |                             APIMessage.ErrorMessage.ILLEGAL_API_ARGUMENT | ||||||
|                                 .of("The LMS for this Exam has no SEB restriction feature"))); |                                     .of("The LMS for this Exam has no SEB restriction feature")); | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 if (this.examSessionService.hasActiveSEBClientConnections(exam.id)) { |                 if (this.examSessionService.hasActiveSEBClientConnections(exam.id)) { | ||||||
|                 return Result.ofError(new APIMessageException( |                     throw new APIMessageException( | ||||||
|                             APIMessage.ErrorMessage.INTEGRITY_VALIDATION |                             APIMessage.ErrorMessage.INTEGRITY_VALIDATION | ||||||
|                                 .of("Exam currently has active SEB Client connections."))); |                                     .of("Exam currently has active SEB Client connections.")); | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|  |                 // TODO double check before setSEBRestriction | ||||||
|                 return this.checkNoActiveSEBClientConnections(exam) |                 return this.checkNoActiveSEBClientConnections(exam) | ||||||
|                         .flatMap(this.sebRestrictionService::applySEBClientRestriction) |                         .flatMap(this.sebRestrictionService::applySEBClientRestriction) | ||||||
|                     .flatMap(e -> this.examDAO.setSEBRestriction(exam.id, restrict)); |                         .flatMap(e -> this.examDAO.setSEBRestriction(exam.id, restrict)) | ||||||
|  |                         .getOrThrow(); | ||||||
|             } else { |             } else { | ||||||
|                 return this.sebRestrictionService.releaseSEBClientRestriction(exam) |                 return this.sebRestrictionService.releaseSEBClientRestriction(exam) | ||||||
|                     .flatMap(e -> this.examDAO.setSEBRestriction(exam.id, restrict)); |                         .flatMap(e -> this.examDAO.setSEBRestriction(exam.id, restrict)) | ||||||
|  |                         .getOrThrow(); | ||||||
|             } |             } | ||||||
|  |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     static Function<Collection<Exam>, List<Exam>> pageSort(final String sort) { |     static Function<Collection<Exam>, List<Exam>> pageSort(final String sort) { | ||||||
|  |  | ||||||
|  | @ -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.LmsSetup; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult; | import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult; | ||||||
| import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; | 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.datalayer.batis.mapper.LmsSetupRecordDynamicSqlSupport; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.PaginationService; | import ch.ethz.seb.sebserver.webservice.servicelayer.PaginationService; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.AuthorizationService; | 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.LmsSetupDAO; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO; | 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.LmsAPIService; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.LmsSetupChangeEvent; |  | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationService; | import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationService; | ||||||
| 
 | 
 | ||||||
| @WebServiceProfile | @WebServiceProfile | ||||||
|  | @ -134,10 +132,4 @@ public class LmsSetupController extends ActivatableEntityController<LmsSetup, Lm | ||||||
|         return new LmsSetup(null, postParams); |         return new LmsSetup(null, postParams); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |  | ||||||
|     protected Result<LmsSetup> notifySaved(final LmsSetup entity) { |  | ||||||
|         this.applicationEventPublisher.publishEvent(new LmsSetupChangeEvent(entity)); |  | ||||||
|         return super.notifySaved(entity); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -193,7 +193,7 @@ public class ModelObjectJSONGenerator { | ||||||
|                 1L, 1L, 1L, "externalId", "name", "description", DateTime.now(), DateTime.now(), |                 1L, 1L, 1L, "externalId", "name", "description", DateTime.now(), DateTime.now(), | ||||||
|                 "startURL", ExamType.BYOD, "owner", |                 "startURL", ExamType.BYOD, "owner", | ||||||
|                 Arrays.asList("user1", "user2"), |                 Arrays.asList("user1", "user2"), | ||||||
|                 ExamStatus.RUNNING, "browserExamKeys", true, null); |                 ExamStatus.RUNNING, false, "browserExamKeys", true, null); | ||||||
|         System.out.println(domainObject.getClass().getSimpleName() + ":"); |         System.out.println(domainObject.getClass().getSimpleName() + ":"); | ||||||
|         System.out.println(writerWithDefaultPrettyPrinter.writeValueAsString(domainObject)); |         System.out.println(writerWithDefaultPrettyPrinter.writeValueAsString(domainObject)); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -850,6 +850,7 @@ public class UseCasesIntegrationTest extends GuiIntegrationTest { | ||||||
|                 null, |                 null, | ||||||
|                 Utils.immutableCollectionOf(userId), |                 Utils.immutableCollectionOf(userId), | ||||||
|                 ExamStatus.RUNNING, |                 ExamStatus.RUNNING, | ||||||
|  |                 false, | ||||||
|                 null, |                 null, | ||||||
|                 true, |                 true, | ||||||
|                 null); |                 null); | ||||||
|  |  | ||||||
|  | @ -64,6 +64,7 @@ public class ExamAPITest extends AdministrationAPIIntegrationTester { | ||||||
|                         exam.owner, |                         exam.owner, | ||||||
|                         Arrays.asList("user5"), |                         Arrays.asList("user5"), | ||||||
|                         null, |                         null, | ||||||
|  |                         false, | ||||||
|                         null, |                         null, | ||||||
|                         true, |                         true, | ||||||
|                         null)) |                         null)) | ||||||
|  | @ -94,6 +95,7 @@ public class ExamAPITest extends AdministrationAPIIntegrationTester { | ||||||
|                         exam.owner, |                         exam.owner, | ||||||
|                         Arrays.asList("user2"), |                         Arrays.asList("user2"), | ||||||
|                         null, |                         null, | ||||||
|  |                         false, | ||||||
|                         null, |                         null, | ||||||
|                         true, |                         true, | ||||||
|                         null)) |                         null)) | ||||||
|  |  | ||||||
|  | @ -10,7 +10,10 @@ package ch.ethz.seb.sebserver.webservice.integration.api.admin; | ||||||
| 
 | 
 | ||||||
| import static org.junit.Assert.*; | import static org.junit.Assert.*; | ||||||
| 
 | 
 | ||||||
|  | import org.junit.After; | ||||||
|  | import org.junit.Before; | ||||||
| import org.junit.Test; | import org.junit.Test; | ||||||
|  | import org.springframework.beans.factory.annotation.Autowired; | ||||||
| import org.springframework.http.HttpMethod; | import org.springframework.http.HttpMethod; | ||||||
| import org.springframework.http.HttpStatus; | import org.springframework.http.HttpStatus; | ||||||
| import org.springframework.test.context.jdbc.Sql; | 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.Exam.ExamType; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.exam.QuizData; | import ch.ethz.seb.sebserver.gbl.model.exam.QuizData; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; | import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; | ||||||
|  | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService; | ||||||
| 
 | 
 | ||||||
| @Sql(scripts = { "classpath:schema-test.sql", "classpath:data-test.sql" }) |  | ||||||
| public class ExamImportTest extends AdministrationAPIIntegrationTester { | 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 |     @Test | ||||||
|     public void testImportFromQuiz() throws Exception { |     public void testImportFromQuiz() throws Exception { | ||||||
|         // create new active LmsSetup Mock with seb-admin |         // create new active LmsSetup Mock with seb-admin | ||||||
|  | @ -57,7 +75,7 @@ public class ExamImportTest extends AdministrationAPIIntegrationTester { | ||||||
|                 this, |                 this, | ||||||
|                 getSebAdminAccess(), |                 getSebAdminAccess(), | ||||||
|                 getSebAdminAccess(), |                 getSebAdminAccess(), | ||||||
|                 "LmsSetupMock", |                 "LmsSetupMock1", | ||||||
|                 "quiz2", |                 "quiz2", | ||||||
|                 ExamType.MANAGED, |                 ExamType.MANAGED, | ||||||
|                 "user5"); |                 "user5"); | ||||||
|  | @ -76,7 +94,7 @@ public class ExamImportTest extends AdministrationAPIIntegrationTester { | ||||||
|                     this, |                     this, | ||||||
|                     getAdminInstitution2Access(), |                     getAdminInstitution2Access(), | ||||||
|                     getAdminInstitution2Access(), |                     getAdminInstitution2Access(), | ||||||
|                     "LmsSetupMock", |                     "LmsSetupMock2", | ||||||
|                     "quiz2", |                     "quiz2", | ||||||
|                     ExamType.MANAGED, |                     ExamType.MANAGED, | ||||||
|                     "user7"); |                     "user7"); | ||||||
|  | @ -90,7 +108,7 @@ public class ExamImportTest extends AdministrationAPIIntegrationTester { | ||||||
|                 this, |                 this, | ||||||
|                 getAdminInstitution2Access(), |                 getAdminInstitution2Access(), | ||||||
|                 getExamAdmin1(), // this exam administrator is on Institution 2 |                 getExamAdmin1(), // this exam administrator is on Institution 2 | ||||||
|                 "LmsSetupMock2", |                 "LmsSetupMock3", | ||||||
|                 "quiz2", |                 "quiz2", | ||||||
|                 ExamType.MANAGED, |                 ExamType.MANAGED, | ||||||
|                 "user7"); |                 "user7"); | ||||||
|  | @ -108,7 +126,7 @@ public class ExamImportTest extends AdministrationAPIIntegrationTester { | ||||||
|                     this, |                     this, | ||||||
|                     getAdminInstitution1Access(), |                     getAdminInstitution1Access(), | ||||||
|                     getExamAdmin1(), // this exam administrator is on Institution 2 |                     getExamAdmin1(), // this exam administrator is on Institution 2 | ||||||
|                     "LmsSetupMock", |                     "LmsSetupMock4", | ||||||
|                     "quiz2", |                     "quiz2", | ||||||
|                     ExamType.MANAGED, |                     ExamType.MANAGED, | ||||||
|                     "user7"); |                     "user7"); | ||||||
|  |  | ||||||
|  | @ -71,7 +71,6 @@ public class MoodleCourseAccessTest { | ||||||
| 
 | 
 | ||||||
|         final MoodleCourseAccess moodleCourseAccess = new MoodleCourseAccess( |         final MoodleCourseAccess moodleCourseAccess = new MoodleCourseAccess( | ||||||
|                 new JSONMapper(), |                 new JSONMapper(), | ||||||
|                 null, |  | ||||||
|                 moodleRestTemplateFactory, |                 moodleRestTemplateFactory, | ||||||
|                 null, |                 null, | ||||||
|                 mock(AsyncService.class), |                 mock(AsyncService.class), | ||||||
|  | @ -120,7 +119,6 @@ public class MoodleCourseAccessTest { | ||||||
| 
 | 
 | ||||||
|         final MoodleCourseAccess moodleCourseAccess = new MoodleCourseAccess( |         final MoodleCourseAccess moodleCourseAccess = new MoodleCourseAccess( | ||||||
|                 new JSONMapper(), |                 new JSONMapper(), | ||||||
|                 null, |  | ||||||
|                 moodleRestTemplateFactory, |                 moodleRestTemplateFactory, | ||||||
|                 null, |                 null, | ||||||
|                 mock(AsyncService.class), |                 mock(AsyncService.class), | ||||||
|  | @ -143,7 +141,6 @@ public class MoodleCourseAccessTest { | ||||||
| 
 | 
 | ||||||
|         final MoodleCourseAccess moodleCourseAccess = new MoodleCourseAccess( |         final MoodleCourseAccess moodleCourseAccess = new MoodleCourseAccess( | ||||||
|                 new JSONMapper(), |                 new JSONMapper(), | ||||||
|                 null, |  | ||||||
|                 moodleRestTemplateFactory, |                 moodleRestTemplateFactory, | ||||||
|                 null, |                 null, | ||||||
|                 mock(AsyncService.class), |                 mock(AsyncService.class), | ||||||
|  | @ -165,7 +162,6 @@ public class MoodleCourseAccessTest { | ||||||
| 
 | 
 | ||||||
|         final MoodleCourseAccess moodleCourseAccess = new MoodleCourseAccess( |         final MoodleCourseAccess moodleCourseAccess = new MoodleCourseAccess( | ||||||
|                 new JSONMapper(), |                 new JSONMapper(), | ||||||
|                 null, |  | ||||||
|                 moodleRestTemplateFactory, |                 moodleRestTemplateFactory, | ||||||
|                 null, |                 null, | ||||||
|                 mock(AsyncService.class), |                 mock(AsyncService.class), | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		
		Reference in a new issue
	
	 anhefti
						anhefti