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