SEBSERV-301 implementation
This commit is contained in:
		
							parent
							
								
									d866b219fa
								
							
						
					
					
						commit
						aa040fc615
					
				
					 27 changed files with 2210 additions and 1566 deletions
				
			
		|  | @ -71,8 +71,6 @@ public final class Exam implements GrantEntity { | ||||||
|     public static final String ADDITIONAL_ATTR_SIGNATURE_KEY_CERT_ALIAS = "SIGNATURE_KEY_CERT_ALIAS"; |     public static final String ADDITIONAL_ATTR_SIGNATURE_KEY_CERT_ALIAS = "SIGNATURE_KEY_CERT_ALIAS"; | ||||||
|     /** This attribute name is used to store the per exam generated app-signature-key encryption salt */ |     /** This attribute name is used to store the per exam generated app-signature-key encryption salt */ | ||||||
|     public static final String ADDITIONAL_ATTR_SIGNATURE_KEY_SALT = "SIGNATURE_KEY_SALT"; |     public static final String ADDITIONAL_ATTR_SIGNATURE_KEY_SALT = "SIGNATURE_KEY_SALT"; | ||||||
|     /** This attribute name is used to store the per Moolde(plugin) exam generated alternative BEK */ |  | ||||||
|     public static final String ADDITIONAL_ATTR_ALTERNATIVE_SEB_BEK = "ALTERNATIVE_SEB_BEK"; |  | ||||||
| 
 | 
 | ||||||
|     public enum ExamStatus { |     public enum ExamStatus { | ||||||
|         UP_COMING, |         UP_COMING, | ||||||
|  |  | ||||||
|  | @ -11,6 +11,7 @@ package ch.ethz.seb.sebserver.gbl.model.exam; | ||||||
| import java.util.Collections; | import java.util.Collections; | ||||||
| import java.util.Comparator; | import java.util.Comparator; | ||||||
| import java.util.Map; | import java.util.Map; | ||||||
|  | import java.util.Objects; | ||||||
| 
 | 
 | ||||||
| import org.apache.commons.lang3.StringUtils; | import org.apache.commons.lang3.StringUtils; | ||||||
| import org.joda.time.DateTime; | import org.joda.time.DateTime; | ||||||
|  | @ -211,6 +212,23 @@ public final class QuizData implements GrantEntity { | ||||||
|         return this.additionalAttributes; |         return this.additionalAttributes; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     @Override | ||||||
|  |     public int hashCode() { | ||||||
|  |         return Objects.hash(this.id, this.lmsSetupId); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public boolean equals(final Object obj) { | ||||||
|  |         if (this == obj) | ||||||
|  |             return true; | ||||||
|  |         if (obj == null) | ||||||
|  |             return false; | ||||||
|  |         if (getClass() != obj.getClass()) | ||||||
|  |             return false; | ||||||
|  |         final QuizData other = (QuizData) obj; | ||||||
|  |         return Objects.equals(this.id, other.id) && Objects.equals(this.lmsSetupId, other.lmsSetupId); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     @Override |     @Override | ||||||
|     public String toString() { |     public String toString() { | ||||||
|         final StringBuilder builder = new StringBuilder(); |         final StringBuilder builder = new StringBuilder(); | ||||||
|  |  | ||||||
|  | @ -10,6 +10,9 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.exam; | ||||||
| 
 | 
 | ||||||
| public interface ExamConfigurationValueService { | public interface ExamConfigurationValueService { | ||||||
| 
 | 
 | ||||||
|  |     public static final String CONFIG_ATTR_NAME_QUIT_LINK = "quitURL"; | ||||||
|  |     public static final String CONFIG_ATTR_NAME_QUIT_SECRET = "hashedQuitPassword"; | ||||||
|  | 
 | ||||||
|     /** Get the actual SEB settings attribute value for the exam configuration mapped as default configuration |     /** Get the actual SEB settings attribute value for the exam configuration mapped as default configuration | ||||||
|      * to the given exam |      * to the given exam | ||||||
|      * |      * | ||||||
|  | @ -18,4 +21,8 @@ public interface ExamConfigurationValueService { | ||||||
|      * @return The current value of the above SEB settings attribute and given exam. */ |      * @return The current value of the above SEB settings attribute and given exam. */ | ||||||
|     String getMappedDefaultConfigAttributeValue(Long examId, String configAttributeName); |     String getMappedDefaultConfigAttributeValue(Long examId, String configAttributeName); | ||||||
| 
 | 
 | ||||||
|  |     String getQuitSecret(Long examId); | ||||||
|  | 
 | ||||||
|  |     String getQuitLink(Long examId); | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -276,7 +276,7 @@ public class ExamAdminServiceImpl implements ExamAdminService { | ||||||
|                     this.additionalAttributesDAO.saveAdditionalAttribute( |                     this.additionalAttributesDAO.saveAdditionalAttribute( | ||||||
|                             EntityType.EXAM, |                             EntityType.EXAM, | ||||||
|                             exam.id, |                             exam.id, | ||||||
|                             Exam.ADDITIONAL_ATTR_ALTERNATIVE_SEB_BEK, |                             SEBRestrictionService.ADDITIONAL_ATTR_ALTERNATIVE_SEB_BEK, | ||||||
|                             moodleBEK).getOrThrow(); |                             moodleBEK).getOrThrow(); | ||||||
|                 } catch (final Exception e) { |                 } catch (final Exception e) { | ||||||
|                     log.error("Failed to create additional moodle SEB BEK attribute: ", e); |                     log.error("Failed to create additional moodle SEB BEK attribute: ", e); | ||||||
|  |  | ||||||
|  | @ -8,17 +8,23 @@ | ||||||
| 
 | 
 | ||||||
| package ch.ethz.seb.sebserver.webservice.servicelayer.exam.impl; | package ch.ethz.seb.sebserver.webservice.servicelayer.exam.impl; | ||||||
| 
 | 
 | ||||||
|  | import org.apache.commons.lang3.StringUtils; | ||||||
| import org.slf4j.Logger; | import org.slf4j.Logger; | ||||||
| import org.slf4j.LoggerFactory; | import org.slf4j.LoggerFactory; | ||||||
|  | import org.springframework.context.annotation.Lazy; | ||||||
| import org.springframework.stereotype.Service; | import org.springframework.stereotype.Service; | ||||||
| 
 | 
 | ||||||
|  | import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; | ||||||
|  | import ch.ethz.seb.sebserver.gbl.util.Cryptor; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationAttributeDAO; | import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationAttributeDAO; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationDAO; | import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationDAO; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationValueDAO; | import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationValueDAO; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamConfigurationMapDAO; | import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamConfigurationMapDAO; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamConfigurationValueService; | import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamConfigurationValueService; | ||||||
| 
 | 
 | ||||||
|  | @Lazy | ||||||
| @Service | @Service | ||||||
|  | @WebServiceProfile | ||||||
| public class ExamConfigurationValueServiceImpl implements ExamConfigurationValueService { | public class ExamConfigurationValueServiceImpl implements ExamConfigurationValueService { | ||||||
| 
 | 
 | ||||||
|     private static final Logger log = LoggerFactory.getLogger(ExamConfigurationValueServiceImpl.class); |     private static final Logger log = LoggerFactory.getLogger(ExamConfigurationValueServiceImpl.class); | ||||||
|  | @ -27,17 +33,20 @@ public class ExamConfigurationValueServiceImpl implements ExamConfigurationValue | ||||||
|     private final ConfigurationDAO configurationDAO; |     private final ConfigurationDAO configurationDAO; | ||||||
|     private final ConfigurationAttributeDAO configurationAttributeDAO; |     private final ConfigurationAttributeDAO configurationAttributeDAO; | ||||||
|     private final ConfigurationValueDAO configurationValueDAO; |     private final ConfigurationValueDAO configurationValueDAO; | ||||||
|  |     private final Cryptor cryptor; | ||||||
| 
 | 
 | ||||||
|     public ExamConfigurationValueServiceImpl( |     public ExamConfigurationValueServiceImpl( | ||||||
|             final ExamConfigurationMapDAO examConfigurationMapDAO, |             final ExamConfigurationMapDAO examConfigurationMapDAO, | ||||||
|             final ConfigurationDAO configurationDAO, |             final ConfigurationDAO configurationDAO, | ||||||
|             final ConfigurationAttributeDAO configurationAttributeDAO, |             final ConfigurationAttributeDAO configurationAttributeDAO, | ||||||
|             final ConfigurationValueDAO configurationValueDAO) { |             final ConfigurationValueDAO configurationValueDAO, | ||||||
|  |             final Cryptor cryptor) { | ||||||
| 
 | 
 | ||||||
|         this.examConfigurationMapDAO = examConfigurationMapDAO; |         this.examConfigurationMapDAO = examConfigurationMapDAO; | ||||||
|         this.configurationDAO = configurationDAO; |         this.configurationDAO = configurationDAO; | ||||||
|         this.configurationAttributeDAO = configurationAttributeDAO; |         this.configurationAttributeDAO = configurationAttributeDAO; | ||||||
|         this.configurationValueDAO = configurationValueDAO; |         this.configurationValueDAO = configurationValueDAO; | ||||||
|  |         this.cryptor = cryptor; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|  | @ -66,4 +75,45 @@ public class ExamConfigurationValueServiceImpl implements ExamConfigurationValue | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     @Override | ||||||
|  |     public String getQuitSecret(final Long examId) { | ||||||
|  |         try { | ||||||
|  | 
 | ||||||
|  |             final String quitSecretEncrypted = getMappedDefaultConfigAttributeValue( | ||||||
|  |                     examId, | ||||||
|  |                     CONFIG_ATTR_NAME_QUIT_SECRET); | ||||||
|  | 
 | ||||||
|  |             if (StringUtils.isNotEmpty(quitSecretEncrypted)) { | ||||||
|  |                 try { | ||||||
|  | 
 | ||||||
|  |                     return this.cryptor | ||||||
|  |                             .decrypt(quitSecretEncrypted) | ||||||
|  |                             .getOrThrow() | ||||||
|  |                             .toString(); | ||||||
|  | 
 | ||||||
|  |                 } catch (final Exception e) { | ||||||
|  |                     log.error("Failed to decrypt quitSecret: ", e); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } catch (final Exception e) { | ||||||
|  |             log.error("Failed to get SEB restriction with quit secret: ", e); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return null; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public String getQuitLink(final Long examId) { | ||||||
|  |         try { | ||||||
|  | 
 | ||||||
|  |             return getMappedDefaultConfigAttributeValue( | ||||||
|  |                     examId, | ||||||
|  |                     CONFIG_ATTR_NAME_QUIT_LINK); | ||||||
|  | 
 | ||||||
|  |         } catch (final Exception e) { | ||||||
|  |             log.error("Failed to get SEB restriction with quit link: ", e); | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -8,8 +8,8 @@ | ||||||
| 
 | 
 | ||||||
| package ch.ethz.seb.sebserver.webservice.servicelayer.lms; | package ch.ethz.seb.sebserver.webservice.servicelayer.lms; | ||||||
| 
 | 
 | ||||||
| import java.util.ArrayList; |  | ||||||
| import java.util.Collection; | import java.util.Collection; | ||||||
|  | import java.util.HashSet; | ||||||
| import java.util.List; | import java.util.List; | ||||||
| import java.util.Set; | import java.util.Set; | ||||||
| 
 | 
 | ||||||
|  | @ -113,10 +113,20 @@ public interface CourseAccessAPI { | ||||||
|      * @return Result referencing to the Chapters model for the given course or to an error when happened. */ |      * @return Result referencing to the Chapters model for the given course or to an error when happened. */ | ||||||
|     Result<Chapters> getCourseChapters(String courseId); |     Result<Chapters> getCourseChapters(String courseId); | ||||||
| 
 | 
 | ||||||
|  |     /** This is used to buffer fetch results of asynchronous LMS quiz data fetch processes. | ||||||
|  |      * An asynchronous LMS quiz data fetch processes will buffer its fetch results within this buffer | ||||||
|  |      * during processing and a request can get already buffered results on a none-blocking manner. | ||||||
|  |      * | ||||||
|  |      * Use it like a Future but with the ability to get already fetched data. */ | ||||||
|     static class AsyncQuizFetchBuffer { |     static class AsyncQuizFetchBuffer { | ||||||
|         public List<QuizData> buffer = new ArrayList<>(); | 
 | ||||||
|  |         /** The buffer set where already fetched data is stored and can be get */ | ||||||
|  |         public Set<QuizData> buffer = new HashSet<>(); | ||||||
|  |         /** Indicates whether the asynchronous fetch is still running or has finished */ | ||||||
|         public boolean finished = false; |         public boolean finished = false; | ||||||
|  |         /** Indicates if the fetch is been canceled. Set this to true to cancel the asynchronous process */ | ||||||
|         public boolean canceled = false; |         public boolean canceled = false; | ||||||
|  |         /** Reference to an error when the asynchronous fetch stopped with an error */ | ||||||
|         public Exception error = null; |         public Exception error = null; | ||||||
| 
 | 
 | ||||||
|         public void finish() { |         public void finish() { | ||||||
|  |  | ||||||
|  | @ -18,6 +18,9 @@ public interface SEBRestrictionService { | ||||||
|     String SEB_RESTRICTION_ADDITIONAL_PROPERTY_NAME_PREFIX = "sebRestrictionProp_"; |     String SEB_RESTRICTION_ADDITIONAL_PROPERTY_NAME_PREFIX = "sebRestrictionProp_"; | ||||||
| 
 | 
 | ||||||
|     String SEB_RESTRICTION_ADDITIONAL_PROPERTY_CONFIG_KEY = "config_key"; |     String SEB_RESTRICTION_ADDITIONAL_PROPERTY_CONFIG_KEY = "config_key"; | ||||||
|  |     /** This attribute name is used to store the per Moolde(plugin) exam generated alternative BEK */ | ||||||
|  |     String ADDITIONAL_ATTR_ALTERNATIVE_SEB_BEK = | ||||||
|  |             SEB_RESTRICTION_ADDITIONAL_PROPERTY_NAME_PREFIX + "ALTERNATIVE_SEB_BEK"; | ||||||
| 
 | 
 | ||||||
|     /** Get the LmsAPIService that is used by the SEBRestrictionService */ |     /** Get the LmsAPIService that is used by the SEBRestrictionService */ | ||||||
|     LmsAPIService getLmsAPIService(); |     LmsAPIService getLmsAPIService(); | ||||||
|  |  | ||||||
|  | @ -98,9 +98,14 @@ public abstract class AbstractCachedCourseAccess { | ||||||
| 
 | 
 | ||||||
|     /** Put all QuizData to short time cache. |     /** Put all QuizData to short time cache. | ||||||
|      * |      * | ||||||
|      * @param quizData Collection of QuizData */ |      * @param quizData Collection of QuizData | ||||||
|     protected void putToCache(final Collection<QuizData> quizData) { |      * @return the given collection of QuizData */ | ||||||
|  |     protected final Collection<QuizData> putToCache(final Collection<QuizData> quizData) { | ||||||
|  |         if (quizData == null || quizData.isEmpty()) { | ||||||
|  |             return quizData; | ||||||
|  |         } | ||||||
|         quizData.stream().forEach(q -> this.cache.put(createCacheKey(q.id), q)); |         quizData.stream().forEach(q -> this.cache.put(createCacheKey(q.id), q)); | ||||||
|  |         return quizData; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     protected void evict(final String id) { |     protected void evict(final String id) { | ||||||
|  |  | ||||||
|  | @ -0,0 +1,263 @@ | ||||||
|  | /* | ||||||
|  |  * Copyright (c) 2022 ETH Zürich, Educational Development and Technology (LET) | ||||||
|  |  * | ||||||
|  |  * This Source Code Form is subject to the terms of the Mozilla Public | ||||||
|  |  * License, v. 2.0. If a copy of the MPL was not distributed with this | ||||||
|  |  * file, You can obtain one at http://mozilla.org/MPL/2.0/. | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle; | ||||||
|  | 
 | ||||||
|  | import java.util.ArrayList; | ||||||
|  | import java.util.Collections; | ||||||
|  | import java.util.HashMap; | ||||||
|  | import java.util.HashSet; | ||||||
|  | import java.util.List; | ||||||
|  | import java.util.Map; | ||||||
|  | import java.util.Set; | ||||||
|  | import java.util.UUID; | ||||||
|  | import java.util.stream.Collectors; | ||||||
|  | 
 | ||||||
|  | import org.springframework.http.HttpEntity; | ||||||
|  | import org.springframework.http.HttpHeaders; | ||||||
|  | import org.springframework.http.MediaType; | ||||||
|  | import org.springframework.util.LinkedMultiValueMap; | ||||||
|  | import org.springframework.util.MultiValueMap; | ||||||
|  | import org.springframework.web.util.UriComponentsBuilder; | ||||||
|  | 
 | ||||||
|  | import com.fasterxml.jackson.core.JsonProcessingException; | ||||||
|  | 
 | ||||||
|  | import ch.ethz.seb.sebserver.gbl.api.JSONMapper; | ||||||
|  | import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType; | ||||||
|  | import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult; | ||||||
|  | import ch.ethz.seb.sebserver.gbl.util.Result; | ||||||
|  | import ch.ethz.seb.sebserver.gbl.util.Utils; | ||||||
|  | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier; | ||||||
|  | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.plugin.MoodlePluginCourseAccess; | ||||||
|  | 
 | ||||||
|  | public class MockupRestTemplateFactory implements MoodleRestTemplateFactory { | ||||||
|  | 
 | ||||||
|  |     private final APITemplateDataSupplier apiTemplateDataSupplier; | ||||||
|  | 
 | ||||||
|  |     public MockupRestTemplateFactory(final APITemplateDataSupplier apiTemplateDataSupplier) { | ||||||
|  |         this.apiTemplateDataSupplier = apiTemplateDataSupplier; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public LmsSetupTestResult test() { | ||||||
|  |         return LmsSetupTestResult.ofOkay(LmsType.MOODLE_PLUGIN); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public APITemplateDataSupplier getApiTemplateDataSupplier() { | ||||||
|  |         return this.apiTemplateDataSupplier; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public Set<String> getKnownTokenAccessPaths() { | ||||||
|  |         final Set<String> paths = new HashSet<>(); | ||||||
|  |         paths.add(MoodleAPIRestTemplate.MOODLE_DEFAULT_TOKEN_REQUEST_PATH); | ||||||
|  |         return paths; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public Result<MoodleAPIRestTemplate> createRestTemplate() { | ||||||
|  |         return Result.of(new MockupMoodleRestTemplate(this.apiTemplateDataSupplier.getLmsSetup().lmsApiUrl)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public Result<MoodleAPIRestTemplate> createRestTemplate(final String accessTokenPath) { | ||||||
|  |         return Result.of(new MockupMoodleRestTemplate(this.apiTemplateDataSupplier.getLmsSetup().lmsApiUrl)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static final class MockupMoodleRestTemplate implements MoodleAPIRestTemplate { | ||||||
|  | 
 | ||||||
|  |         private final String accessToken = UUID.randomUUID().toString(); | ||||||
|  |         private final String url; | ||||||
|  | 
 | ||||||
|  |         public MockupMoodleRestTemplate(final String url) { | ||||||
|  |             this.url = url; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         @Override | ||||||
|  |         public String getService() { | ||||||
|  |             return "mockup-service"; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         @Override | ||||||
|  |         public void setService(final String service) { | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         @Override | ||||||
|  |         public CharSequence getAccessToken() { | ||||||
|  |             System.out.println("***** getAccessToken: " + this.accessToken); | ||||||
|  |             return this.accessToken; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         @Override | ||||||
|  |         public void testAPIConnection(final String... functions) { | ||||||
|  |             System.out.println("***** testAPIConnection functions: " + functions); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         @Override | ||||||
|  |         public String callMoodleAPIFunction(final String functionName) { | ||||||
|  |             return callMoodleAPIFunction(functionName, null, null); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         @Override | ||||||
|  |         public String callMoodleAPIFunction( | ||||||
|  |                 final String functionName, | ||||||
|  |                 final MultiValueMap<String, String> queryAttributes) { | ||||||
|  |             return callMoodleAPIFunction(functionName, null, queryAttributes); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         @Override | ||||||
|  |         public String callMoodleAPIFunction( | ||||||
|  |                 final String functionName, | ||||||
|  |                 final MultiValueMap<String, String> queryParams, | ||||||
|  |                 final MultiValueMap<String, String> queryAttributes) { | ||||||
|  | 
 | ||||||
|  |             final UriComponentsBuilder queryParam = UriComponentsBuilder | ||||||
|  |                     .fromHttpUrl(this.url + MOODLE_DEFAULT_REST_API_PATH) | ||||||
|  |                     .queryParam(REST_REQUEST_TOKEN_NAME, this.accessToken) | ||||||
|  |                     .queryParam(REST_REQUEST_FUNCTION_NAME, functionName) | ||||||
|  |                     .queryParam(REST_REQUEST_FORMAT_NAME, "json"); | ||||||
|  | 
 | ||||||
|  |             if (queryParams != null && !queryParams.isEmpty()) { | ||||||
|  |                 queryParam.queryParams(queryParams); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             final boolean usePOST = queryAttributes != null && !queryAttributes.isEmpty(); | ||||||
|  |             HttpEntity<?> functionReqEntity; | ||||||
|  |             if (usePOST) { | ||||||
|  |                 final HttpHeaders headers = new HttpHeaders(); | ||||||
|  |                 headers.set( | ||||||
|  |                         HttpHeaders.CONTENT_TYPE, | ||||||
|  |                         MediaType.APPLICATION_FORM_URLENCODED_VALUE); | ||||||
|  | 
 | ||||||
|  |                 final String body = Utils.toAppFormUrlEncodedBody(queryAttributes); | ||||||
|  |                 functionReqEntity = new HttpEntity<>(body, headers); | ||||||
|  | 
 | ||||||
|  |             } else { | ||||||
|  |                 functionReqEntity = new HttpEntity<>(new LinkedMultiValueMap<>()); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             System.out.println("***** callMoodleAPIFunction HttpEntity: " + functionReqEntity); | ||||||
|  | 
 | ||||||
|  |             // TODO return json | ||||||
|  |             if (MoodlePluginCourseAccess.COURSES_API_FUNCTION_NAME.equals(functionName)) { | ||||||
|  |                 return respondCourses(queryAttributes); | ||||||
|  |             } else if (MoodlePluginCourseAccess.QUIZZES_BY_COURSES_API_FUNCTION_NAME.equals(functionName)) { | ||||||
|  |                 return respondQuizzes(queryAttributes); | ||||||
|  |             } else if (MoodlePluginCourseAccess.USERS_API_FUNCTION_NAME.equals(functionName)) { | ||||||
|  |                 return respondUsers(queryAttributes); | ||||||
|  |             } else { | ||||||
|  |                 throw new RuntimeException("Unknown function: " + functionName); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         private static final class MockCD { | ||||||
|  |             public final String id; | ||||||
|  |             public final String shortname; | ||||||
|  |             public final String categoryid; | ||||||
|  |             public final String fullname; | ||||||
|  |             public final String displayname; | ||||||
|  |             public final String idnumber; | ||||||
|  |             public final Long startdate; // unix-time seconds UTC | ||||||
|  |             public final Long enddate; // unix-time seconds UTC | ||||||
|  |             public final Long timecreated; // unix-time seconds UTC | ||||||
|  |             public final boolean visible; | ||||||
|  | 
 | ||||||
|  |             public MockCD(final String num) { | ||||||
|  |                 this.id = num; | ||||||
|  |                 this.shortname = "c" + num; | ||||||
|  |                 this.categoryid = "mock"; | ||||||
|  |                 this.fullname = "course" + num; | ||||||
|  |                 this.displayname = this.fullname; | ||||||
|  |                 this.idnumber = "i" + num; | ||||||
|  |                 this.startdate = Long.valueOf(num); | ||||||
|  |                 this.enddate = null; | ||||||
|  |                 this.timecreated = Long.valueOf(num); | ||||||
|  |                 this.visible = true; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         private static final class MockQ { | ||||||
|  |             public final String id; | ||||||
|  |             public final String coursemodule; | ||||||
|  |             public final String course; | ||||||
|  |             public final String name; | ||||||
|  |             public final String intro; | ||||||
|  |             public final Long timeopen; // unix-time seconds UTC | ||||||
|  |             public final Long timeclose; // unix-time seconds UTC | ||||||
|  | 
 | ||||||
|  |             public MockQ(final String courseId, final String num) { | ||||||
|  |                 this.id = num; | ||||||
|  |                 this.coursemodule = courseId; | ||||||
|  |                 this.course = courseId; | ||||||
|  |                 this.name = "quiz " + num; | ||||||
|  |                 this.intro = this.name; | ||||||
|  |                 this.timeopen = Long.valueOf(num); | ||||||
|  |                 this.timeclose = null; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         private String respondCourses(final MultiValueMap<String, String> queryAttributes) { | ||||||
|  |             try { | ||||||
|  |                 final List<String> ids = queryAttributes.get(MoodlePluginCourseAccess.CRITERIA_COURSE_IDS); | ||||||
|  |                 final String from = queryAttributes.getFirst(MoodlePluginCourseAccess.CRITERIA_LIMIT_FROM); | ||||||
|  |                 System.out.println("************* from: " + from); | ||||||
|  |                 final List<MockCD> courses; | ||||||
|  |                 if (ids != null && !ids.isEmpty()) { | ||||||
|  |                     courses = ids.stream().map(id -> new MockCD(id)).collect(Collectors.toList()); | ||||||
|  |                 } else if (from != null && Integer.valueOf(from) < 11) { | ||||||
|  |                     courses = new ArrayList<>(); | ||||||
|  |                     final int num = (Integer.valueOf(from) > 0) ? 10 : 1; | ||||||
|  |                     for (int i = 0; i < 10; i++) { | ||||||
|  |                         courses.add(new MockCD(String.valueOf(num + i))); | ||||||
|  |                     } | ||||||
|  |                 } else { | ||||||
|  |                     courses = new ArrayList<>(); | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 final Map<String, Object> response = new HashMap<>(); | ||||||
|  |                 response.put("courses", courses); | ||||||
|  |                 final JSONMapper jsonMapper = new JSONMapper(); | ||||||
|  |                 final String result = jsonMapper.writeValueAsString(response); | ||||||
|  |                 System.out.println("******** courses response: " + result); | ||||||
|  |                 return result; | ||||||
|  |             } catch (final JsonProcessingException e) { | ||||||
|  |                 e.printStackTrace(); | ||||||
|  |                 return ""; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         private String respondQuizzes(final MultiValueMap<String, String> queryAttributes) { | ||||||
|  |             try { | ||||||
|  |                 final List<String> ids = queryAttributes.get(MoodlePluginCourseAccess.CRITERIA_COURSE_IDS); | ||||||
|  |                 final List<MockQ> quizzes; | ||||||
|  |                 if (ids != null && !ids.isEmpty()) { | ||||||
|  |                     quizzes = ids.stream().map(id -> new MockQ(id, "10" + id)).collect(Collectors.toList()); | ||||||
|  |                 } else { | ||||||
|  |                     quizzes = Collections.emptyList(); | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 final Map<String, Object> response = new HashMap<>(); | ||||||
|  |                 response.put("quizzes", quizzes); | ||||||
|  |                 final JSONMapper jsonMapper = new JSONMapper(); | ||||||
|  |                 final String result = jsonMapper.writeValueAsString(response); | ||||||
|  |                 System.out.println("******** quizzes response: " + result); | ||||||
|  |                 return result; | ||||||
|  |             } catch (final JsonProcessingException e) { | ||||||
|  |                 e.printStackTrace(); | ||||||
|  |                 return ""; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         private String respondUsers(final MultiValueMap<String, String> queryAttributes) { | ||||||
|  |             // TODO | ||||||
|  |             return ""; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,68 @@ | ||||||
|  | /* | ||||||
|  |  * Copyright (c) 2022 ETH Zürich, Educational Development and Technology (LET) | ||||||
|  |  * | ||||||
|  |  * This Source Code Form is subject to the terms of the Mozilla Public | ||||||
|  |  * License, v. 2.0. If a copy of the MPL was not distributed with this | ||||||
|  |  * file, You can obtain one at http://mozilla.org/MPL/2.0/. | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle; | ||||||
|  | 
 | ||||||
|  | import org.slf4j.Logger; | ||||||
|  | import org.slf4j.LoggerFactory; | ||||||
|  | import org.springframework.context.annotation.Lazy; | ||||||
|  | import org.springframework.stereotype.Service; | ||||||
|  | 
 | ||||||
|  | import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult; | ||||||
|  | import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; | ||||||
|  | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.plugin.MoodlePluginCourseAccess; | ||||||
|  | 
 | ||||||
|  | @Lazy | ||||||
|  | @Service | ||||||
|  | @WebServiceProfile | ||||||
|  | public class MoodlePluginCheck { | ||||||
|  | 
 | ||||||
|  |     private static final Logger log = LoggerFactory.getLogger(MoodlePluginCheck.class); | ||||||
|  | 
 | ||||||
|  |     /** Used to check if the moodle SEB Server plugin is available for a given LMSSetup. | ||||||
|  |      * | ||||||
|  |      * @param lmsSetup The LMS Setup | ||||||
|  |      * @return true if the SEB Server plugin is available */ | ||||||
|  |     public boolean checkPluginAvailable(final MoodleRestTemplateFactory restTemplateFactory) { | ||||||
|  |         try { | ||||||
|  | 
 | ||||||
|  |             log.info("Check Moodle SEB Server Plugin available..."); | ||||||
|  | 
 | ||||||
|  |             final LmsSetupTestResult test = restTemplateFactory.test(); | ||||||
|  | 
 | ||||||
|  |             if (!test.isOk()) { | ||||||
|  |                 log.warn("Failed to check Moodle SEB Server Plugin because of invalid LMS Setup: ", test); | ||||||
|  |                 return false; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             final MoodleAPIRestTemplate restTemplate = restTemplateFactory | ||||||
|  |                     .createRestTemplate() | ||||||
|  |                     .getOrThrow(); | ||||||
|  | 
 | ||||||
|  |             try { | ||||||
|  |                 restTemplate.testAPIConnection( | ||||||
|  |                         MoodlePluginCourseAccess.COURSES_API_FUNCTION_NAME, | ||||||
|  |                         MoodlePluginCourseAccess.QUIZZES_BY_COURSES_API_FUNCTION_NAME, | ||||||
|  |                         MoodlePluginCourseAccess.USERS_API_FUNCTION_NAME); | ||||||
|  |             } catch (final Exception e) { | ||||||
|  |                 log.info("Moodle SEB Server Plugin not available: {}", e.getMessage()); | ||||||
|  |                 return false; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             log.info("Moodle SEB Server Plugin not available for: {}", | ||||||
|  |                     restTemplateFactory.getApiTemplateDataSupplier().getLmsSetup()); | ||||||
|  | 
 | ||||||
|  |             return true; | ||||||
|  | 
 | ||||||
|  |         } catch (final Exception e) { | ||||||
|  |             log.error("Failed to check Moodle SEB Server Plugin because of unexpected error: ", e); | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -1,463 +1,29 @@ | ||||||
| /* | /* | ||||||
|  * Copyright (c) 2020 ETH Zürich, Educational Development and Technology (LET) |  * Copyright (c) 2022 ETH Zürich, Educational Development and Technology (LET) | ||||||
|  * |  * | ||||||
|  * This Source Code Form is subject to the terms of the Mozilla Public |  * This Source Code Form is subject to the terms of the Mozilla Public | ||||||
|  * License, v. 2.0. If a copy of the MPL was not distributed with this |  * License, v. 2.0. If a copy of the MPL was not distributed with this | ||||||
|  * file, You can obtain one at http://mozilla.org/MPL/2.0/. |  * file, You can obtain one at http://mozilla.org/MPL/2.0/. | ||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
| package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle; | package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle; | ||||||
| 
 | 
 | ||||||
| import java.util.ArrayList; | import java.util.Set; | ||||||
| import java.util.Arrays; | 
 | ||||||
| import java.util.Collection; | import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult; | ||||||
| import java.util.Collections; | import ch.ethz.seb.sebserver.gbl.util.Result; | ||||||
| import java.util.HashMap; | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier; | ||||||
| import java.util.HashSet; | 
 | ||||||
| import java.util.List; | public interface MoodleRestTemplateFactory { | ||||||
| import java.util.Map; | 
 | ||||||
| import java.util.Set; |     LmsSetupTestResult test(); | ||||||
| import java.util.function.Function; | 
 | ||||||
| import java.util.stream.Collectors; |     APITemplateDataSupplier getApiTemplateDataSupplier(); | ||||||
| 
 | 
 | ||||||
| import org.apache.commons.lang3.StringUtils; |     Set<String> getKnownTokenAccessPaths(); | ||||||
| import org.slf4j.Logger; | 
 | ||||||
| import org.slf4j.LoggerFactory; |     Result<MoodleAPIRestTemplate> createRestTemplate(); | ||||||
| import org.springframework.http.HttpEntity; | 
 | ||||||
| import org.springframework.http.HttpHeaders; |     Result<MoodleAPIRestTemplate> createRestTemplate(final String accessTokenPath); | ||||||
| import org.springframework.http.HttpMethod; | 
 | ||||||
| import org.springframework.http.HttpStatus; | } | ||||||
| import org.springframework.http.MediaType; |  | ||||||
| import org.springframework.http.ResponseEntity; |  | ||||||
| import org.springframework.http.client.ClientHttpRequestFactory; |  | ||||||
| import org.springframework.util.LinkedMultiValueMap; |  | ||||||
| import org.springframework.util.MultiValueMap; |  | ||||||
| import org.springframework.web.client.RestTemplate; |  | ||||||
| import org.springframework.web.util.UriComponentsBuilder; |  | ||||||
| 
 |  | ||||||
| import com.fasterxml.jackson.annotation.JsonCreator; |  | ||||||
| import com.fasterxml.jackson.annotation.JsonIgnoreProperties; |  | ||||||
| import com.fasterxml.jackson.annotation.JsonProperty; |  | ||||||
| 
 |  | ||||||
| import ch.ethz.seb.sebserver.ClientHttpRequestFactoryService; |  | ||||||
| import ch.ethz.seb.sebserver.gbl.api.APIMessage; |  | ||||||
| import ch.ethz.seb.sebserver.gbl.api.JSONMapper; |  | ||||||
| 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.Domain.LMS_SETUP; |  | ||||||
| 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.util.Result; |  | ||||||
| import ch.ethz.seb.sebserver.gbl.util.Utils; |  | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier; |  | ||||||
| 
 |  | ||||||
| public class MoodleRestTemplateFactory { |  | ||||||
| 
 |  | ||||||
|     private static final Logger log = LoggerFactory.getLogger(MoodleRestTemplateFactory.class); |  | ||||||
| 
 |  | ||||||
|     public final JSONMapper jsonMapper; |  | ||||||
|     public final APITemplateDataSupplier apiTemplateDataSupplier; |  | ||||||
|     public final ClientHttpRequestFactoryService clientHttpRequestFactoryService; |  | ||||||
|     public final ClientCredentialService clientCredentialService; |  | ||||||
|     public final Set<String> knownTokenAccessPaths; |  | ||||||
| 
 |  | ||||||
|     public MoodleRestTemplateFactory( |  | ||||||
|             final JSONMapper jsonMapper, |  | ||||||
|             final APITemplateDataSupplier apiTemplateDataSupplier, |  | ||||||
|             final ClientCredentialService clientCredentialService, |  | ||||||
|             final ClientHttpRequestFactoryService clientHttpRequestFactoryService, |  | ||||||
|             final String[] alternativeTokenRequestPaths) { |  | ||||||
| 
 |  | ||||||
|         this.jsonMapper = jsonMapper; |  | ||||||
|         this.apiTemplateDataSupplier = apiTemplateDataSupplier; |  | ||||||
|         this.clientCredentialService = clientCredentialService; |  | ||||||
|         this.clientHttpRequestFactoryService = clientHttpRequestFactoryService; |  | ||||||
| 
 |  | ||||||
|         final Set<String> paths = new HashSet<>(); |  | ||||||
|         paths.add(MoodleAPIRestTemplate.MOODLE_DEFAULT_TOKEN_REQUEST_PATH); |  | ||||||
|         if (alternativeTokenRequestPaths != null) { |  | ||||||
|             paths.addAll(Arrays.asList(alternativeTokenRequestPaths)); |  | ||||||
|         } |  | ||||||
|         this.knownTokenAccessPaths = Utils.immutableSetOf(paths); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     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(lmsSetup.lmsApiUrl)) { |  | ||||||
|             missingAttrs.add(APIMessage.fieldValidationError( |  | ||||||
|                     LMS_SETUP.ATTR_LMS_URL, |  | ||||||
|                     "lmsSetup:lmsUrl:notNull")); |  | ||||||
|         } else { |  | ||||||
|             // try to connect to the url |  | ||||||
|             if (!Utils.pingHost(lmsSetup.lmsApiUrl)) { |  | ||||||
|                 missingAttrs.add(APIMessage.fieldValidationError( |  | ||||||
|                         LMS_SETUP.ATTR_LMS_URL, |  | ||||||
|                         "lmsSetup:lmsUrl:url.invalid")); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         if (StringUtils.isBlank(lmsSetup.lmsRestApiToken)) { |  | ||||||
|             if (!credentials.hasClientId()) { |  | ||||||
|                 missingAttrs.add(APIMessage.fieldValidationError( |  | ||||||
|                         LMS_SETUP.ATTR_LMS_CLIENTNAME, |  | ||||||
|                         "lmsSetup:lmsClientname:notNull")); |  | ||||||
|             } |  | ||||||
|             if (!credentials.hasSecret()) { |  | ||||||
|                 missingAttrs.add(APIMessage.fieldValidationError( |  | ||||||
|                         LMS_SETUP.ATTR_LMS_CLIENTSECRET, |  | ||||||
|                         "lmsSetup:lmsClientsecret:notNull")); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         if (!missingAttrs.isEmpty()) { |  | ||||||
|             return LmsSetupTestResult.ofMissingAttributes(LmsType.MOODLE, missingAttrs); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return LmsSetupTestResult.ofOkay(LmsType.MOODLE); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public 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: {}({})", |  | ||||||
|                                 lmsSetup.name, |  | ||||||
|                                 lmsSetup.id, |  | ||||||
|                                 result.getError().getMessage()); |  | ||||||
|                     } |  | ||||||
|                     return result; |  | ||||||
|                 }) |  | ||||||
|                 .filter(Result::hasValue) |  | ||||||
|                 .findFirst() |  | ||||||
|                 .orElse(Result.ofRuntimeError( |  | ||||||
|                         "Failed to gain any access for LMS " + |  | ||||||
|                                 lmsSetup.name + "(" + lmsSetup.id + |  | ||||||
|                                 ") on paths: " + this.knownTokenAccessPaths)); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public Result<MoodleAPIRestTemplate> createRestTemplate(final String accessTokenPath) { |  | ||||||
| 
 |  | ||||||
|         final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup(); |  | ||||||
| 
 |  | ||||||
|         return Result.tryCatch(() -> { |  | ||||||
|             final ClientCredentials credentials = this.apiTemplateDataSupplier.getLmsClientCredentials(); |  | ||||||
|             final ProxyData proxyData = this.apiTemplateDataSupplier.getProxyData(); |  | ||||||
| 
 |  | ||||||
|             final CharSequence plainClientId = credentials.clientId; |  | ||||||
|             final CharSequence plainClientSecret = this.clientCredentialService |  | ||||||
|                     .getPlainClientSecret(credentials) |  | ||||||
|                     .getOrThrow(); |  | ||||||
| 
 |  | ||||||
|             final MoodleAPIRestTemplateImpl restTemplate = new MoodleAPIRestTemplateImpl( |  | ||||||
|                     this.jsonMapper, |  | ||||||
|                     this.apiTemplateDataSupplier, |  | ||||||
|                     lmsSetup.lmsApiUrl, |  | ||||||
|                     accessTokenPath, |  | ||||||
|                     lmsSetup.lmsRestApiToken, |  | ||||||
|                     plainClientId, |  | ||||||
|                     plainClientSecret); |  | ||||||
| 
 |  | ||||||
|             final ClientHttpRequestFactory clientHttpRequestFactory = this.clientHttpRequestFactoryService |  | ||||||
|                     .getClientHttpRequestFactory(proxyData) |  | ||||||
|                     .getOrThrow(); |  | ||||||
| 
 |  | ||||||
|             restTemplate.setRequestFactory(clientHttpRequestFactory); |  | ||||||
|             final CharSequence accessToken = restTemplate.getAccessToken(); |  | ||||||
| 
 |  | ||||||
|             if (accessToken == null) { |  | ||||||
|                 throw new RuntimeException("Failed to get access token for LMS " + |  | ||||||
|                         lmsSetup.name + "(" + lmsSetup.id + |  | ||||||
|                         ") on path: " + accessTokenPath); |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             return restTemplate; |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public static class MoodleAPIRestTemplateImpl extends RestTemplate implements MoodleAPIRestTemplate { |  | ||||||
| 
 |  | ||||||
|         private static final String REST_API_TEST_FUNCTION = "core_webservice_get_site_info"; |  | ||||||
| 
 |  | ||||||
|         final JSONMapper jsonMapper; |  | ||||||
|         final APITemplateDataSupplier apiTemplateDataSupplier; |  | ||||||
| 
 |  | ||||||
|         private final String serverURL; |  | ||||||
|         private final String tokenPath; |  | ||||||
| 
 |  | ||||||
|         private CharSequence accessToken; |  | ||||||
| 
 |  | ||||||
|         private final Map<String, String> tokenReqURIVars; |  | ||||||
|         private final HttpEntity<?> tokenReqEntity = new HttpEntity<>(new LinkedMultiValueMap<>()); |  | ||||||
| 
 |  | ||||||
|         protected MoodleAPIRestTemplateImpl( |  | ||||||
|                 final JSONMapper jsonMapper, |  | ||||||
|                 final APITemplateDataSupplier apiTemplateDataSupplier, |  | ||||||
|                 final String serverURL, |  | ||||||
|                 final String tokenPath, |  | ||||||
|                 final CharSequence accessToken, |  | ||||||
|                 final CharSequence username, |  | ||||||
|                 final CharSequence password) { |  | ||||||
| 
 |  | ||||||
|             this.jsonMapper = jsonMapper; |  | ||||||
|             this.apiTemplateDataSupplier = apiTemplateDataSupplier; |  | ||||||
| 
 |  | ||||||
|             this.serverURL = serverURL; |  | ||||||
|             this.tokenPath = tokenPath; |  | ||||||
|             this.accessToken = StringUtils.isNotBlank(accessToken) ? accessToken : null; |  | ||||||
| 
 |  | ||||||
|             this.tokenReqURIVars = new HashMap<>(); |  | ||||||
|             this.tokenReqURIVars.put(URI_VAR_USER_NAME, String.valueOf(username)); |  | ||||||
|             this.tokenReqURIVars.put(URI_VAR_PASSWORD, String.valueOf(password)); |  | ||||||
|             this.tokenReqURIVars.put(URI_VAR_SERVICE, "moodle_mobile_app"); |  | ||||||
| 
 |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         @Override |  | ||||||
|         public String getService() { |  | ||||||
|             return this.tokenReqURIVars.get(URI_VAR_SERVICE); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         @Override |  | ||||||
|         public void setService(final String service) { |  | ||||||
|             this.tokenReqURIVars.put(URI_VAR_SERVICE, service); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         @Override |  | ||||||
|         public CharSequence getAccessToken() { |  | ||||||
|             if (this.accessToken == null) { |  | ||||||
|                 requestAccessToken(); |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             return this.accessToken; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         @Override |  | ||||||
|         public void testAPIConnection(final String... functions) { |  | ||||||
|             try { |  | ||||||
|                 final String apiInfo = this.callMoodleAPIFunction(REST_API_TEST_FUNCTION); |  | ||||||
|                 final WebserviceInfo webserviceInfo = this.jsonMapper.readValue( |  | ||||||
|                         apiInfo, |  | ||||||
|                         WebserviceInfo.class); |  | ||||||
| 
 |  | ||||||
|                 if (StringUtils.isBlank(webserviceInfo.username) || StringUtils.isBlank(webserviceInfo.userid)) { |  | ||||||
|                     throw new RuntimeException("Invalid WebserviceInfo: " + webserviceInfo); |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 if (functions != null) { |  | ||||||
| 
 |  | ||||||
|                     final List<String> missingAPIFunctions = Arrays.stream(functions) |  | ||||||
|                             .filter(f -> !webserviceInfo.functions.containsKey(f)) |  | ||||||
|                             .collect(Collectors.toList()); |  | ||||||
| 
 |  | ||||||
|                     if (!missingAPIFunctions.isEmpty()) { |  | ||||||
|                         throw new RuntimeException("Missing Moodle Webservice API functions: " + missingAPIFunctions); |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|             } catch (final RuntimeException re) { |  | ||||||
|                 throw re; |  | ||||||
|             } catch (final Exception e) { |  | ||||||
|                 throw new RuntimeException("Failed to test Moodle rest API: ", e); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         @Override |  | ||||||
|         public String callMoodleAPIFunction(final String functionName) { |  | ||||||
|             return callMoodleAPIFunction(functionName, null, null); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         @Override |  | ||||||
|         public String callMoodleAPIFunction( |  | ||||||
|                 final String functionName, |  | ||||||
|                 final MultiValueMap<String, String> queryAttributes) { |  | ||||||
|             return callMoodleAPIFunction(functionName, null, queryAttributes); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         @Override |  | ||||||
|         public String callMoodleAPIFunction( |  | ||||||
|                 final String functionName, |  | ||||||
|                 final MultiValueMap<String, String> queryParams, |  | ||||||
|                 final MultiValueMap<String, String> queryAttributes) { |  | ||||||
| 
 |  | ||||||
|             getAccessToken(); |  | ||||||
| 
 |  | ||||||
|             final UriComponentsBuilder queryParam = UriComponentsBuilder |  | ||||||
|                     .fromHttpUrl(this.serverURL + MOODLE_DEFAULT_REST_API_PATH) |  | ||||||
|                     .queryParam(REST_REQUEST_TOKEN_NAME, this.accessToken) |  | ||||||
|                     .queryParam(REST_REQUEST_FUNCTION_NAME, functionName) |  | ||||||
|                     .queryParam(REST_REQUEST_FORMAT_NAME, "json"); |  | ||||||
| 
 |  | ||||||
|             if (queryParams != null && !queryParams.isEmpty()) { |  | ||||||
|                 queryParam.queryParams(queryParams); |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             final boolean usePOST = queryAttributes != null && !queryAttributes.isEmpty(); |  | ||||||
|             HttpEntity<?> functionReqEntity; |  | ||||||
|             if (usePOST) { |  | ||||||
|                 final HttpHeaders headers = new HttpHeaders(); |  | ||||||
|                 headers.set( |  | ||||||
|                         HttpHeaders.CONTENT_TYPE, |  | ||||||
|                         MediaType.APPLICATION_FORM_URLENCODED_VALUE); |  | ||||||
| 
 |  | ||||||
|                 final String body = Utils.toAppFormUrlEncodedBody(queryAttributes); |  | ||||||
|                 functionReqEntity = new HttpEntity<>(body, headers); |  | ||||||
| 
 |  | ||||||
|             } else { |  | ||||||
|                 functionReqEntity = new HttpEntity<>(new LinkedMultiValueMap<>()); |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             final ResponseEntity<String> response = super.exchange( |  | ||||||
|                     queryParam.toUriString(), |  | ||||||
|                     usePOST ? HttpMethod.POST : HttpMethod.GET, |  | ||||||
|                     functionReqEntity, |  | ||||||
|                     String.class); |  | ||||||
| 
 |  | ||||||
|             final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup(); |  | ||||||
|             if (response.getStatusCode() != HttpStatus.OK) { |  | ||||||
|                 throw new RuntimeException( |  | ||||||
|                         "Failed to call Moodle webservice API function: " + functionName + " lms setup: " + |  | ||||||
|                                 lmsSetup + " response: " + response.getBody()); |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             final String body = response.getBody(); |  | ||||||
| 
 |  | ||||||
|             // NOTE: for some unknown reason, Moodles API error responses come with a 200 OK response HTTP Status |  | ||||||
|             //       So this is a special Moodle specific error handling here... |  | ||||||
|             if (body.startsWith("{exception") || body.contains("\"exception\":")) { |  | ||||||
|                 // Reset access token to get new on next call (fix access if token is expired) |  | ||||||
|                 // TODO find a way to verify token invalidity response from Moodle. |  | ||||||
|                 //      Unfortunately there is not a lot of Moodle documentation for the API error handling around. |  | ||||||
|                 this.accessToken = null; |  | ||||||
|                 throw new RuntimeException( |  | ||||||
|                         "Failed to call Moodle webservice API function: " + functionName + " lms setup: " + |  | ||||||
|                                 lmsSetup + " response: " + body); |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             return body; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         private void requestAccessToken() { |  | ||||||
| 
 |  | ||||||
|             final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup(); |  | ||||||
|             try { |  | ||||||
| 
 |  | ||||||
|                 final ResponseEntity<String> response = super.exchange( |  | ||||||
|                         this.serverURL + this.tokenPath, |  | ||||||
|                         HttpMethod.GET, |  | ||||||
|                         this.tokenReqEntity, |  | ||||||
|                         String.class, |  | ||||||
|                         this.tokenReqURIVars); |  | ||||||
| 
 |  | ||||||
|                 if (response.getStatusCode() != HttpStatus.OK) { |  | ||||||
|                     log.error("Failed to gain access token for LMS (Moodle): lmsSetup: {} response: {} : {}", |  | ||||||
|                             lmsSetup, |  | ||||||
|                             response.getStatusCode(), |  | ||||||
|                             response.getBody()); |  | ||||||
|                     throw new RuntimeException("Failed to gain access token for LMS (Moodle): lmsSetup: " + |  | ||||||
|                             lmsSetup + " response: " + response.getBody()); |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 try { |  | ||||||
|                     final MoodleToken moodleToken = this.jsonMapper.readValue( |  | ||||||
|                             response.getBody(), |  | ||||||
|                             MoodleToken.class); |  | ||||||
| 
 |  | ||||||
|                     if (moodleToken == null || moodleToken.token == null) { |  | ||||||
|                         throw new RuntimeException("Access Token request with 200 but no or invalid token body"); |  | ||||||
|                     } else { |  | ||||||
|                         log.info("Successfully get access token from Moodle: {}", |  | ||||||
|                                 lmsSetup); |  | ||||||
|                     } |  | ||||||
| 
 |  | ||||||
|                     this.accessToken = moodleToken.token; |  | ||||||
|                 } catch (final Exception e) { |  | ||||||
|                     log.error("Failed to gain access token for LMS (Moodle): lmsSetup: {} response: {} : {}", |  | ||||||
|                             lmsSetup, |  | ||||||
|                             response.getStatusCode(), |  | ||||||
|                             response.getBody()); |  | ||||||
|                     throw new RuntimeException("Failed to gain access token for LMS (Moodle): lmsSetup: " + |  | ||||||
|                             lmsSetup + " response: " + response.getBody(), e); |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|             } catch (final Exception e) { |  | ||||||
|                 log.error("Failed to gain access token for LMS (Moodle): lmsSetup: {} :", |  | ||||||
|                         lmsSetup, |  | ||||||
|                         e); |  | ||||||
|                 throw new RuntimeException("Failed to gain access token for LMS (Moodle): lmsSetup: " + |  | ||||||
|                         lmsSetup + " cause: " + e.getMessage()); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @JsonIgnoreProperties(ignoreUnknown = true) |  | ||||||
|     private final static class MoodleToken { |  | ||||||
|         final String token; |  | ||||||
|         @SuppressWarnings("unused") |  | ||||||
|         final String privatetoken; |  | ||||||
| 
 |  | ||||||
|         @JsonCreator |  | ||||||
|         protected MoodleToken( |  | ||||||
|                 @JsonProperty(value = "token") final String token, |  | ||||||
|                 @JsonProperty(value = "privatetoken", required = false) final String privatetoken) { |  | ||||||
| 
 |  | ||||||
|             this.token = token; |  | ||||||
|             this.privatetoken = privatetoken; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @JsonIgnoreProperties(ignoreUnknown = true) |  | ||||||
|     private final static class WebserviceInfo { |  | ||||||
|         String username; |  | ||||||
|         String userid; |  | ||||||
|         Map<String, FunctionInfo> functions; |  | ||||||
| 
 |  | ||||||
|         @JsonCreator |  | ||||||
|         protected WebserviceInfo( |  | ||||||
|                 @JsonProperty(value = "username") final String username, |  | ||||||
|                 @JsonProperty(value = "userid") final String userid, |  | ||||||
|                 @JsonProperty(value = "functions") final Collection<FunctionInfo> functions) { |  | ||||||
| 
 |  | ||||||
|             this.username = username; |  | ||||||
|             this.userid = userid; |  | ||||||
|             this.functions = (functions != null) |  | ||||||
|                     ? functions |  | ||||||
|                             .stream() |  | ||||||
|                             .collect(Collectors.toMap(fi -> fi.name, Function.identity())) |  | ||||||
|                     : Collections.emptyMap(); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @JsonIgnoreProperties(ignoreUnknown = true) |  | ||||||
|     private final static class FunctionInfo { |  | ||||||
|         String name; |  | ||||||
|         @SuppressWarnings("unused") |  | ||||||
|         String version; |  | ||||||
| 
 |  | ||||||
|         @JsonCreator |  | ||||||
|         protected FunctionInfo( |  | ||||||
|                 @JsonProperty(value = "name") final String name, |  | ||||||
|                 @JsonProperty(value = "version") final String version) { |  | ||||||
| 
 |  | ||||||
|             this.name = name; |  | ||||||
|             this.version = version; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -0,0 +1,472 @@ | ||||||
|  | /* | ||||||
|  |  * Copyright (c) 2020 ETH Zürich, Educational Development and Technology (LET) | ||||||
|  |  * | ||||||
|  |  * This Source Code Form is subject to the terms of the Mozilla Public | ||||||
|  |  * License, v. 2.0. If a copy of the MPL was not distributed with this | ||||||
|  |  * file, You can obtain one at http://mozilla.org/MPL/2.0/. | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle; | ||||||
|  | 
 | ||||||
|  | import java.util.ArrayList; | ||||||
|  | import java.util.Arrays; | ||||||
|  | import java.util.Collection; | ||||||
|  | import java.util.Collections; | ||||||
|  | import java.util.HashMap; | ||||||
|  | import java.util.HashSet; | ||||||
|  | import java.util.List; | ||||||
|  | import java.util.Map; | ||||||
|  | import java.util.Set; | ||||||
|  | import java.util.function.Function; | ||||||
|  | import java.util.stream.Collectors; | ||||||
|  | 
 | ||||||
|  | import org.apache.commons.lang3.StringUtils; | ||||||
|  | import org.slf4j.Logger; | ||||||
|  | import org.slf4j.LoggerFactory; | ||||||
|  | import org.springframework.http.HttpEntity; | ||||||
|  | import org.springframework.http.HttpHeaders; | ||||||
|  | import org.springframework.http.HttpMethod; | ||||||
|  | import org.springframework.http.HttpStatus; | ||||||
|  | import org.springframework.http.MediaType; | ||||||
|  | import org.springframework.http.ResponseEntity; | ||||||
|  | import org.springframework.http.client.ClientHttpRequestFactory; | ||||||
|  | import org.springframework.util.LinkedMultiValueMap; | ||||||
|  | import org.springframework.util.MultiValueMap; | ||||||
|  | import org.springframework.web.client.RestTemplate; | ||||||
|  | import org.springframework.web.util.UriComponentsBuilder; | ||||||
|  | 
 | ||||||
|  | import com.fasterxml.jackson.annotation.JsonCreator; | ||||||
|  | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; | ||||||
|  | import com.fasterxml.jackson.annotation.JsonProperty; | ||||||
|  | 
 | ||||||
|  | import ch.ethz.seb.sebserver.ClientHttpRequestFactoryService; | ||||||
|  | import ch.ethz.seb.sebserver.gbl.api.APIMessage; | ||||||
|  | import ch.ethz.seb.sebserver.gbl.api.JSONMapper; | ||||||
|  | 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.Domain.LMS_SETUP; | ||||||
|  | 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.util.Result; | ||||||
|  | import ch.ethz.seb.sebserver.gbl.util.Utils; | ||||||
|  | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier; | ||||||
|  | 
 | ||||||
|  | public class MoodleRestTemplateFactoryImpl implements MoodleRestTemplateFactory { | ||||||
|  | 
 | ||||||
|  |     private static final Logger log = LoggerFactory.getLogger(MoodleRestTemplateFactoryImpl.class); | ||||||
|  | 
 | ||||||
|  |     public final JSONMapper jsonMapper; | ||||||
|  |     public final APITemplateDataSupplier apiTemplateDataSupplier; | ||||||
|  |     public final ClientHttpRequestFactoryService clientHttpRequestFactoryService; | ||||||
|  |     public final ClientCredentialService clientCredentialService; | ||||||
|  |     public final Set<String> knownTokenAccessPaths; | ||||||
|  | 
 | ||||||
|  |     public MoodleRestTemplateFactoryImpl( | ||||||
|  |             final JSONMapper jsonMapper, | ||||||
|  |             final APITemplateDataSupplier apiTemplateDataSupplier, | ||||||
|  |             final ClientCredentialService clientCredentialService, | ||||||
|  |             final ClientHttpRequestFactoryService clientHttpRequestFactoryService, | ||||||
|  |             final String[] alternativeTokenRequestPaths) { | ||||||
|  | 
 | ||||||
|  |         this.jsonMapper = jsonMapper; | ||||||
|  |         this.apiTemplateDataSupplier = apiTemplateDataSupplier; | ||||||
|  |         this.clientCredentialService = clientCredentialService; | ||||||
|  |         this.clientHttpRequestFactoryService = clientHttpRequestFactoryService; | ||||||
|  | 
 | ||||||
|  |         final Set<String> paths = new HashSet<>(); | ||||||
|  |         paths.add(MoodleAPIRestTemplate.MOODLE_DEFAULT_TOKEN_REQUEST_PATH); | ||||||
|  |         if (alternativeTokenRequestPaths != null) { | ||||||
|  |             paths.addAll(Arrays.asList(alternativeTokenRequestPaths)); | ||||||
|  |         } | ||||||
|  |         this.knownTokenAccessPaths = Utils.immutableSetOf(paths); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public Set<String> getKnownTokenAccessPaths() { | ||||||
|  |         return this.knownTokenAccessPaths; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public APITemplateDataSupplier getApiTemplateDataSupplier() { | ||||||
|  |         return this.apiTemplateDataSupplier; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public LmsSetupTestResult test() { | ||||||
|  | 
 | ||||||
|  |         final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup(); | ||||||
|  |         final ClientCredentials credentials = this.apiTemplateDataSupplier.getLmsClientCredentials(); | ||||||
|  | 
 | ||||||
|  |         final List<APIMessage> missingAttrs = new ArrayList<>(); | ||||||
|  |         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(lmsSetup.lmsApiUrl)) { | ||||||
|  |                 missingAttrs.add(APIMessage.fieldValidationError( | ||||||
|  |                         LMS_SETUP.ATTR_LMS_URL, | ||||||
|  |                         "lmsSetup:lmsUrl:url.invalid")); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (StringUtils.isBlank(lmsSetup.lmsRestApiToken)) { | ||||||
|  |             if (!credentials.hasClientId()) { | ||||||
|  |                 missingAttrs.add(APIMessage.fieldValidationError( | ||||||
|  |                         LMS_SETUP.ATTR_LMS_CLIENTNAME, | ||||||
|  |                         "lmsSetup:lmsClientname:notNull")); | ||||||
|  |             } | ||||||
|  |             if (!credentials.hasSecret()) { | ||||||
|  |                 missingAttrs.add(APIMessage.fieldValidationError( | ||||||
|  |                         LMS_SETUP.ATTR_LMS_CLIENTSECRET, | ||||||
|  |                         "lmsSetup:lmsClientsecret:notNull")); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (!missingAttrs.isEmpty()) { | ||||||
|  |             return LmsSetupTestResult.ofMissingAttributes(LmsType.MOODLE, missingAttrs); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return LmsSetupTestResult.ofOkay(LmsType.MOODLE); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public 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: {}({})", | ||||||
|  |                                 lmsSetup.name, | ||||||
|  |                                 lmsSetup.id, | ||||||
|  |                                 result.getError().getMessage()); | ||||||
|  |                     } | ||||||
|  |                     return result; | ||||||
|  |                 }) | ||||||
|  |                 .filter(Result::hasValue) | ||||||
|  |                 .findFirst() | ||||||
|  |                 .orElse(Result.ofRuntimeError( | ||||||
|  |                         "Failed to gain any access for LMS " + | ||||||
|  |                                 lmsSetup.name + "(" + lmsSetup.id + | ||||||
|  |                                 ") on paths: " + this.knownTokenAccessPaths)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public Result<MoodleAPIRestTemplate> createRestTemplate(final String accessTokenPath) { | ||||||
|  | 
 | ||||||
|  |         final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup(); | ||||||
|  | 
 | ||||||
|  |         return Result.tryCatch(() -> { | ||||||
|  |             final ClientCredentials credentials = this.apiTemplateDataSupplier.getLmsClientCredentials(); | ||||||
|  |             final ProxyData proxyData = this.apiTemplateDataSupplier.getProxyData(); | ||||||
|  | 
 | ||||||
|  |             final CharSequence plainClientId = credentials.clientId; | ||||||
|  |             final CharSequence plainClientSecret = this.clientCredentialService | ||||||
|  |                     .getPlainClientSecret(credentials) | ||||||
|  |                     .getOrThrow(); | ||||||
|  | 
 | ||||||
|  |             final MoodleAPIRestTemplateImpl restTemplate = new MoodleAPIRestTemplateImpl( | ||||||
|  |                     this.jsonMapper, | ||||||
|  |                     this.apiTemplateDataSupplier, | ||||||
|  |                     lmsSetup.lmsApiUrl, | ||||||
|  |                     accessTokenPath, | ||||||
|  |                     lmsSetup.lmsRestApiToken, | ||||||
|  |                     plainClientId, | ||||||
|  |                     plainClientSecret); | ||||||
|  | 
 | ||||||
|  |             final ClientHttpRequestFactory clientHttpRequestFactory = this.clientHttpRequestFactoryService | ||||||
|  |                     .getClientHttpRequestFactory(proxyData) | ||||||
|  |                     .getOrThrow(); | ||||||
|  | 
 | ||||||
|  |             restTemplate.setRequestFactory(clientHttpRequestFactory); | ||||||
|  |             final CharSequence accessToken = restTemplate.getAccessToken(); | ||||||
|  | 
 | ||||||
|  |             if (accessToken == null) { | ||||||
|  |                 throw new RuntimeException("Failed to get access token for LMS " + | ||||||
|  |                         lmsSetup.name + "(" + lmsSetup.id + | ||||||
|  |                         ") on path: " + accessTokenPath); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             return restTemplate; | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static class MoodleAPIRestTemplateImpl extends RestTemplate implements MoodleAPIRestTemplate { | ||||||
|  | 
 | ||||||
|  |         private static final String REST_API_TEST_FUNCTION = "core_webservice_get_site_info"; | ||||||
|  | 
 | ||||||
|  |         final JSONMapper jsonMapper; | ||||||
|  |         final APITemplateDataSupplier apiTemplateDataSupplier; | ||||||
|  | 
 | ||||||
|  |         private final String serverURL; | ||||||
|  |         private final String tokenPath; | ||||||
|  | 
 | ||||||
|  |         private CharSequence accessToken; | ||||||
|  | 
 | ||||||
|  |         private final Map<String, String> tokenReqURIVars; | ||||||
|  |         private final HttpEntity<?> tokenReqEntity = new HttpEntity<>(new LinkedMultiValueMap<>()); | ||||||
|  | 
 | ||||||
|  |         protected MoodleAPIRestTemplateImpl( | ||||||
|  |                 final JSONMapper jsonMapper, | ||||||
|  |                 final APITemplateDataSupplier apiTemplateDataSupplier, | ||||||
|  |                 final String serverURL, | ||||||
|  |                 final String tokenPath, | ||||||
|  |                 final CharSequence accessToken, | ||||||
|  |                 final CharSequence username, | ||||||
|  |                 final CharSequence password) { | ||||||
|  | 
 | ||||||
|  |             this.jsonMapper = jsonMapper; | ||||||
|  |             this.apiTemplateDataSupplier = apiTemplateDataSupplier; | ||||||
|  | 
 | ||||||
|  |             this.serverURL = serverURL; | ||||||
|  |             this.tokenPath = tokenPath; | ||||||
|  |             this.accessToken = StringUtils.isNotBlank(accessToken) ? accessToken : null; | ||||||
|  | 
 | ||||||
|  |             this.tokenReqURIVars = new HashMap<>(); | ||||||
|  |             this.tokenReqURIVars.put(URI_VAR_USER_NAME, String.valueOf(username)); | ||||||
|  |             this.tokenReqURIVars.put(URI_VAR_PASSWORD, String.valueOf(password)); | ||||||
|  |             this.tokenReqURIVars.put(URI_VAR_SERVICE, "moodle_mobile_app"); | ||||||
|  | 
 | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         @Override | ||||||
|  |         public String getService() { | ||||||
|  |             return this.tokenReqURIVars.get(URI_VAR_SERVICE); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         @Override | ||||||
|  |         public void setService(final String service) { | ||||||
|  |             this.tokenReqURIVars.put(URI_VAR_SERVICE, service); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         @Override | ||||||
|  |         public CharSequence getAccessToken() { | ||||||
|  |             if (this.accessToken == null) { | ||||||
|  |                 requestAccessToken(); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             return this.accessToken; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         @Override | ||||||
|  |         public void testAPIConnection(final String... functions) { | ||||||
|  |             try { | ||||||
|  |                 final String apiInfo = this.callMoodleAPIFunction(REST_API_TEST_FUNCTION); | ||||||
|  |                 final WebserviceInfo webserviceInfo = this.jsonMapper.readValue( | ||||||
|  |                         apiInfo, | ||||||
|  |                         WebserviceInfo.class); | ||||||
|  | 
 | ||||||
|  |                 if (StringUtils.isBlank(webserviceInfo.username) || StringUtils.isBlank(webserviceInfo.userid)) { | ||||||
|  |                     throw new RuntimeException("Invalid WebserviceInfo: " + webserviceInfo); | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 if (functions != null) { | ||||||
|  | 
 | ||||||
|  |                     final List<String> missingAPIFunctions = Arrays.stream(functions) | ||||||
|  |                             .filter(f -> !webserviceInfo.functions.containsKey(f)) | ||||||
|  |                             .collect(Collectors.toList()); | ||||||
|  | 
 | ||||||
|  |                     if (!missingAPIFunctions.isEmpty()) { | ||||||
|  |                         throw new RuntimeException("Missing Moodle Webservice API functions: " + missingAPIFunctions); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |             } catch (final RuntimeException re) { | ||||||
|  |                 throw re; | ||||||
|  |             } catch (final Exception e) { | ||||||
|  |                 throw new RuntimeException("Failed to test Moodle rest API: ", e); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         @Override | ||||||
|  |         public String callMoodleAPIFunction(final String functionName) { | ||||||
|  |             return callMoodleAPIFunction(functionName, null, null); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         @Override | ||||||
|  |         public String callMoodleAPIFunction( | ||||||
|  |                 final String functionName, | ||||||
|  |                 final MultiValueMap<String, String> queryAttributes) { | ||||||
|  |             return callMoodleAPIFunction(functionName, null, queryAttributes); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         @Override | ||||||
|  |         public String callMoodleAPIFunction( | ||||||
|  |                 final String functionName, | ||||||
|  |                 final MultiValueMap<String, String> queryParams, | ||||||
|  |                 final MultiValueMap<String, String> queryAttributes) { | ||||||
|  | 
 | ||||||
|  |             getAccessToken(); | ||||||
|  | 
 | ||||||
|  |             final UriComponentsBuilder queryParam = UriComponentsBuilder | ||||||
|  |                     .fromHttpUrl(this.serverURL + MOODLE_DEFAULT_REST_API_PATH) | ||||||
|  |                     .queryParam(REST_REQUEST_TOKEN_NAME, this.accessToken) | ||||||
|  |                     .queryParam(REST_REQUEST_FUNCTION_NAME, functionName) | ||||||
|  |                     .queryParam(REST_REQUEST_FORMAT_NAME, "json"); | ||||||
|  | 
 | ||||||
|  |             if (queryParams != null && !queryParams.isEmpty()) { | ||||||
|  |                 queryParam.queryParams(queryParams); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             final boolean usePOST = queryAttributes != null && !queryAttributes.isEmpty(); | ||||||
|  |             HttpEntity<?> functionReqEntity; | ||||||
|  |             if (usePOST) { | ||||||
|  |                 final HttpHeaders headers = new HttpHeaders(); | ||||||
|  |                 headers.set( | ||||||
|  |                         HttpHeaders.CONTENT_TYPE, | ||||||
|  |                         MediaType.APPLICATION_FORM_URLENCODED_VALUE); | ||||||
|  | 
 | ||||||
|  |                 final String body = Utils.toAppFormUrlEncodedBody(queryAttributes); | ||||||
|  |                 functionReqEntity = new HttpEntity<>(body, headers); | ||||||
|  | 
 | ||||||
|  |             } else { | ||||||
|  |                 functionReqEntity = new HttpEntity<>(new LinkedMultiValueMap<>()); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             final ResponseEntity<String> response = super.exchange( | ||||||
|  |                     queryParam.toUriString(), | ||||||
|  |                     usePOST ? HttpMethod.POST : HttpMethod.GET, | ||||||
|  |                     functionReqEntity, | ||||||
|  |                     String.class); | ||||||
|  | 
 | ||||||
|  |             final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup(); | ||||||
|  |             if (response.getStatusCode() != HttpStatus.OK) { | ||||||
|  |                 throw new RuntimeException( | ||||||
|  |                         "Failed to call Moodle webservice API function: " + functionName + " lms setup: " + | ||||||
|  |                                 lmsSetup + " response: " + response.getBody()); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             final String body = response.getBody(); | ||||||
|  | 
 | ||||||
|  |             // NOTE: for some unknown reason, Moodles API error responses come with a 200 OK response HTTP Status | ||||||
|  |             //       So this is a special Moodle specific error handling here... | ||||||
|  |             if (body.startsWith("{exception") || body.contains("\"exception\":")) { | ||||||
|  |                 // Reset access token to get new on next call (fix access if token is expired) | ||||||
|  |                 // NOTE: find a way to verify token invalidity response from Moodle. | ||||||
|  |                 //      Unfortunately there is not a lot of Moodle documentation for the API error handling around. | ||||||
|  |                 this.accessToken = null; | ||||||
|  |                 throw new RuntimeException( | ||||||
|  |                         "Failed to call Moodle webservice API function: " + functionName + " lms setup: " + | ||||||
|  |                                 lmsSetup + " response: " + body); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             return body; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         private void requestAccessToken() { | ||||||
|  | 
 | ||||||
|  |             final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup(); | ||||||
|  |             try { | ||||||
|  | 
 | ||||||
|  |                 final ResponseEntity<String> response = super.exchange( | ||||||
|  |                         this.serverURL + this.tokenPath, | ||||||
|  |                         HttpMethod.GET, | ||||||
|  |                         this.tokenReqEntity, | ||||||
|  |                         String.class, | ||||||
|  |                         this.tokenReqURIVars); | ||||||
|  | 
 | ||||||
|  |                 if (response.getStatusCode() != HttpStatus.OK) { | ||||||
|  |                     log.error("Failed to gain access token for LMS (Moodle): lmsSetup: {} response: {} : {}", | ||||||
|  |                             lmsSetup, | ||||||
|  |                             response.getStatusCode(), | ||||||
|  |                             response.getBody()); | ||||||
|  |                     throw new RuntimeException("Failed to gain access token for LMS (Moodle): lmsSetup: " + | ||||||
|  |                             lmsSetup + " response: " + response.getBody()); | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 try { | ||||||
|  |                     final MoodleToken moodleToken = this.jsonMapper.readValue( | ||||||
|  |                             response.getBody(), | ||||||
|  |                             MoodleToken.class); | ||||||
|  | 
 | ||||||
|  |                     if (moodleToken == null || moodleToken.token == null) { | ||||||
|  |                         throw new RuntimeException("Access Token request with 200 but no or invalid token body"); | ||||||
|  |                     } else { | ||||||
|  |                         log.info("Successfully get access token from Moodle: {}", | ||||||
|  |                                 lmsSetup); | ||||||
|  |                     } | ||||||
|  | 
 | ||||||
|  |                     this.accessToken = moodleToken.token; | ||||||
|  |                 } catch (final Exception e) { | ||||||
|  |                     log.error("Failed to gain access token for LMS (Moodle): lmsSetup: {} response: {} : {}", | ||||||
|  |                             lmsSetup, | ||||||
|  |                             response.getStatusCode(), | ||||||
|  |                             response.getBody()); | ||||||
|  |                     throw new RuntimeException("Failed to gain access token for LMS (Moodle): lmsSetup: " + | ||||||
|  |                             lmsSetup + " response: " + response.getBody(), e); | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |             } catch (final Exception e) { | ||||||
|  |                 log.error("Failed to gain access token for LMS (Moodle): lmsSetup: {} :", | ||||||
|  |                         lmsSetup, | ||||||
|  |                         e); | ||||||
|  |                 throw new RuntimeException("Failed to gain access token for LMS (Moodle): lmsSetup: " + | ||||||
|  |                         lmsSetup + " cause: " + e.getMessage()); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @JsonIgnoreProperties(ignoreUnknown = true) | ||||||
|  |     private final static class MoodleToken { | ||||||
|  |         final String token; | ||||||
|  |         @SuppressWarnings("unused") | ||||||
|  |         final String privatetoken; | ||||||
|  | 
 | ||||||
|  |         @JsonCreator | ||||||
|  |         protected MoodleToken( | ||||||
|  |                 @JsonProperty(value = "token") final String token, | ||||||
|  |                 @JsonProperty(value = "privatetoken", required = false) final String privatetoken) { | ||||||
|  | 
 | ||||||
|  |             this.token = token; | ||||||
|  |             this.privatetoken = privatetoken; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @JsonIgnoreProperties(ignoreUnknown = true) | ||||||
|  |     private final static class WebserviceInfo { | ||||||
|  |         String username; | ||||||
|  |         String userid; | ||||||
|  |         Map<String, FunctionInfo> functions; | ||||||
|  | 
 | ||||||
|  |         @JsonCreator | ||||||
|  |         protected WebserviceInfo( | ||||||
|  |                 @JsonProperty(value = "username") final String username, | ||||||
|  |                 @JsonProperty(value = "userid") final String userid, | ||||||
|  |                 @JsonProperty(value = "functions") final Collection<FunctionInfo> functions) { | ||||||
|  | 
 | ||||||
|  |             this.username = username; | ||||||
|  |             this.userid = userid; | ||||||
|  |             this.functions = (functions != null) | ||||||
|  |                     ? functions | ||||||
|  |                             .stream() | ||||||
|  |                             .collect(Collectors.toMap(fi -> fi.name, Function.identity())) | ||||||
|  |                     : Collections.emptyMap(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @JsonIgnoreProperties(ignoreUnknown = true) | ||||||
|  |     private final static class FunctionInfo { | ||||||
|  |         String name; | ||||||
|  |         @SuppressWarnings("unused") | ||||||
|  |         String version; | ||||||
|  | 
 | ||||||
|  |         @JsonCreator | ||||||
|  |         protected FunctionInfo( | ||||||
|  |                 @JsonProperty(value = "name") final String name, | ||||||
|  |                 @JsonProperty(value = "version") final String version) { | ||||||
|  | 
 | ||||||
|  |             this.name = name; | ||||||
|  |             this.version = version; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -0,0 +1,518 @@ | ||||||
|  | /* | ||||||
|  |  * Copyright (c) 2022 ETH Zürich, Educational Development and Technology (LET) | ||||||
|  |  * | ||||||
|  |  * This Source Code Form is subject to the terms of the Mozilla Public | ||||||
|  |  * License, v. 2.0. If a copy of the MPL was not distributed with this | ||||||
|  |  * file, You can obtain one at http://mozilla.org/MPL/2.0/. | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle; | ||||||
|  | 
 | ||||||
|  | import java.util.ArrayList; | ||||||
|  | import java.util.Collection; | ||||||
|  | import java.util.HashMap; | ||||||
|  | import java.util.List; | ||||||
|  | import java.util.Map; | ||||||
|  | import java.util.Set; | ||||||
|  | import java.util.function.Predicate; | ||||||
|  | import java.util.regex.Pattern; | ||||||
|  | import java.util.stream.Collectors; | ||||||
|  | 
 | ||||||
|  | import org.apache.commons.lang3.StringUtils; | ||||||
|  | import org.joda.time.DateTime; | ||||||
|  | import org.joda.time.DateTimeZone; | ||||||
|  | import org.slf4j.Logger; | ||||||
|  | import org.slf4j.LoggerFactory; | ||||||
|  | 
 | ||||||
|  | import com.fasterxml.jackson.annotation.JsonCreator; | ||||||
|  | import com.fasterxml.jackson.annotation.JsonIgnore; | ||||||
|  | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; | ||||||
|  | import com.fasterxml.jackson.annotation.JsonProperty; | ||||||
|  | 
 | ||||||
|  | import ch.ethz.seb.sebserver.gbl.Constants; | ||||||
|  | import ch.ethz.seb.sebserver.gbl.model.exam.QuizData; | ||||||
|  | import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; | ||||||
|  | import ch.ethz.seb.sebserver.gbl.util.Utils; | ||||||
|  | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleAPIRestTemplate.Warning; | ||||||
|  | 
 | ||||||
|  | public abstract class MoodleUtils { | ||||||
|  | 
 | ||||||
|  |     private static final Logger log = LoggerFactory.getLogger(MoodleUtils.class); | ||||||
|  | 
 | ||||||
|  |     public static final String getInternalQuizId( | ||||||
|  |             final String quizId, | ||||||
|  |             final String courseId, | ||||||
|  |             final String shortname, | ||||||
|  |             final String idnumber) { | ||||||
|  | 
 | ||||||
|  |         return StringUtils.join( | ||||||
|  |                 new String[] { | ||||||
|  |                         quizId, | ||||||
|  |                         courseId, | ||||||
|  |                         StringUtils.isNotBlank(shortname) ? shortname : Constants.EMPTY_NOTE, | ||||||
|  |                         StringUtils.isNotBlank(idnumber) ? idnumber : Constants.EMPTY_NOTE | ||||||
|  |                 }, | ||||||
|  |                 Constants.COLON); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static final String getQuizId(final String internalQuizId) { | ||||||
|  |         if (StringUtils.isBlank(internalQuizId)) { | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return StringUtils.split(internalQuizId, Constants.COLON)[0]; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static final String getCourseId(final String internalQuizId) { | ||||||
|  |         if (StringUtils.isBlank(internalQuizId)) { | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return StringUtils.split(internalQuizId, Constants.COLON)[1]; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static final String getShortname(final String internalQuizId) { | ||||||
|  |         if (StringUtils.isBlank(internalQuizId)) { | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         final String[] split = StringUtils.split(internalQuizId, Constants.COLON); | ||||||
|  |         if (split.length < 3) { | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         final String shortName = split[2]; | ||||||
|  |         return shortName.equals(Constants.EMPTY_NOTE) ? null : shortName; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static final String getIdnumber(final String internalQuizId) { | ||||||
|  |         if (StringUtils.isBlank(internalQuizId)) { | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  |         final String[] split = StringUtils.split(internalQuizId, Constants.COLON); | ||||||
|  |         if (split.length < 4) { | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         final String idNumber = split[3]; | ||||||
|  |         return idNumber.equals(Constants.EMPTY_NOTE) ? null : idNumber; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static final void logMoodleWarning( | ||||||
|  |             final Collection<Warning> warnings, | ||||||
|  |             final String lmsSetupName, | ||||||
|  |             final String function) { | ||||||
|  | 
 | ||||||
|  |         log.warn( | ||||||
|  |                 "There are warnings from Moodle response: Moodle: {} request: {} warnings: {} warning sample: {}", | ||||||
|  |                 lmsSetupName, | ||||||
|  |                 function, | ||||||
|  |                 warnings.size(), | ||||||
|  |                 warnings.iterator().next().toString()); | ||||||
|  |         if (log.isTraceEnabled()) { | ||||||
|  |             log.trace("All warnings from Moodle: {}", warnings.toString()); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static final Pattern ACCESS_DENIED_PATTERN_1 = | ||||||
|  |             Pattern.compile(Pattern.quote("No access rights"), Pattern.CASE_INSENSITIVE); | ||||||
|  |     private static final Pattern ACCESS_DENIED_PATTERN_2 = | ||||||
|  |             Pattern.compile(Pattern.quote("access denied"), Pattern.CASE_INSENSITIVE); | ||||||
|  | 
 | ||||||
|  |     public static final boolean checkAccessDeniedError(final String courseKeyPageJSON) { | ||||||
|  |         return ACCESS_DENIED_PATTERN_1 | ||||||
|  |                 .matcher(courseKeyPageJSON) | ||||||
|  |                 .find() || | ||||||
|  |                 ACCESS_DENIED_PATTERN_2 | ||||||
|  |                         .matcher(courseKeyPageJSON) | ||||||
|  |                         .find(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static Predicate<CourseData> getCourseFilter() { | ||||||
|  |         final long now = Utils.getSecondsNow(); | ||||||
|  |         return course -> { | ||||||
|  |             if (course.start_date != null | ||||||
|  |                     && course.start_date < Utils.toUnixTimeInSeconds(DateTime.now(DateTimeZone.UTC).minusYears(3))) { | ||||||
|  |                 return false; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (course.end_date == null || course.end_date == 0 || course.end_date > now) { | ||||||
|  |                 return true; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (log.isDebugEnabled()) { | ||||||
|  |                 log.info("remove course {} end_time {} now {}", | ||||||
|  |                         course.short_name, | ||||||
|  |                         course.end_date, | ||||||
|  |                         now); | ||||||
|  |             } | ||||||
|  |             return false; | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static Predicate<CourseQuiz> getQuizFilter() { | ||||||
|  |         final long now = Utils.getSecondsNow(); | ||||||
|  |         return quiz -> { | ||||||
|  |             if (quiz.time_close == null || quiz.time_close == 0 || quiz.time_close > now) { | ||||||
|  |                 return true; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (log.isDebugEnabled()) { | ||||||
|  |                 log.debug("remove quiz {} end_time {} now {}", | ||||||
|  |                         quiz.name, | ||||||
|  |                         quiz.time_close, | ||||||
|  |                         now); | ||||||
|  |             } | ||||||
|  |             return false; | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static List<QuizData> quizDataOf( | ||||||
|  |             final LmsSetup lmsSetup, | ||||||
|  |             final CourseData courseData, | ||||||
|  |             final String uriPrefix, | ||||||
|  |             final boolean prependShortCourseName) { | ||||||
|  | 
 | ||||||
|  |         final Map<String, String> additionalAttrs = new HashMap<>(); | ||||||
|  |         additionalAttrs.put(QuizData.ATTR_ADDITIONAL_CREATION_TIME, String.valueOf(courseData.time_created)); | ||||||
|  |         additionalAttrs.put(QuizData.ATTR_ADDITIONAL_SHORT_NAME, courseData.short_name); | ||||||
|  |         additionalAttrs.put(QuizData.ATTR_ADDITIONAL_ID_NUMBER, courseData.idnumber); | ||||||
|  |         additionalAttrs.put(QuizData.ATTR_ADDITIONAL_FULL_NAME, courseData.full_name); | ||||||
|  |         additionalAttrs.put(QuizData.ATTR_ADDITIONAL_DISPLAY_NAME, courseData.display_name); | ||||||
|  |         additionalAttrs.put(QuizData.ATTR_ADDITIONAL_SUMMARY, courseData.summary); | ||||||
|  | 
 | ||||||
|  |         final List<QuizData> courseAndQuiz = courseData.quizzes | ||||||
|  |                 .stream() | ||||||
|  |                 .map(courseQuizData -> { | ||||||
|  |                     final String startURI = uriPrefix + courseQuizData.course_module; | ||||||
|  |                     additionalAttrs.put(QuizData.ATTR_ADDITIONAL_TIME_LIMIT, String.valueOf(courseQuizData.time_limit)); | ||||||
|  |                     return new QuizData( | ||||||
|  |                             MoodleUtils.getInternalQuizId( | ||||||
|  |                                     courseQuizData.course_module, | ||||||
|  |                                     courseData.id, | ||||||
|  |                                     courseData.short_name, | ||||||
|  |                                     courseData.idnumber), | ||||||
|  |                             lmsSetup.getInstitutionId(), | ||||||
|  |                             lmsSetup.id, | ||||||
|  |                             lmsSetup.getLmsType(), | ||||||
|  |                             (prependShortCourseName) | ||||||
|  |                                     ? courseData.short_name + " : " + courseQuizData.name | ||||||
|  |                                     : courseQuizData.name, | ||||||
|  |                             courseQuizData.intro, | ||||||
|  |                             (courseQuizData.time_open != null && courseQuizData.time_open > 0) | ||||||
|  |                                     ? Utils.toDateTimeUTCUnix(courseQuizData.time_open) | ||||||
|  |                                     : Utils.toDateTimeUTCUnix(courseData.start_date), | ||||||
|  |                             (courseQuizData.time_close != null && courseQuizData.time_close > 0) | ||||||
|  |                                     ? Utils.toDateTimeUTCUnix(courseQuizData.time_close) | ||||||
|  |                                     : Utils.toDateTimeUTCUnix(courseData.end_date), | ||||||
|  |                             startURI, | ||||||
|  |                             additionalAttrs); | ||||||
|  |                 }) | ||||||
|  |                 .collect(Collectors.toList()); | ||||||
|  | 
 | ||||||
|  |         return courseAndQuiz; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static final void fillSelectedQuizzes( | ||||||
|  |             final Set<String> quizIds, | ||||||
|  |             final Map<String, CourseData> finalCourseDataRef, | ||||||
|  |             final CourseQuiz quiz) { | ||||||
|  |         try { | ||||||
|  |             final CourseData course = finalCourseDataRef.get(quiz.course); | ||||||
|  |             if (course != null) { | ||||||
|  |                 final String internalQuizId = MoodleUtils.getInternalQuizId( | ||||||
|  |                         quiz.course_module, | ||||||
|  |                         course.id, | ||||||
|  |                         course.short_name, | ||||||
|  |                         course.idnumber); | ||||||
|  |                 if (quizIds.contains(internalQuizId)) { | ||||||
|  |                     course.quizzes.add(quiz); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } catch (final Exception e) { | ||||||
|  |             log.error("Failed to verify selected quiz for course: {}", e.getMessage()); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // ---- Mapping Classes --- | ||||||
|  | 
 | ||||||
|  |     /** Maps the Moodle course API course data */ | ||||||
|  |     @JsonIgnoreProperties(ignoreUnknown = true) | ||||||
|  |     public static final class CourseData { | ||||||
|  |         public final String id; | ||||||
|  |         public final String short_name; | ||||||
|  |         public final String idnumber; | ||||||
|  |         public final String full_name; | ||||||
|  |         public final String display_name; | ||||||
|  |         public final String summary; | ||||||
|  |         public final Long start_date; // unix-time seconds UTC | ||||||
|  |         public final Long end_date; // unix-time seconds UTC | ||||||
|  |         public final Long time_created; // unix-time seconds UTC | ||||||
|  |         public final String category_id; | ||||||
|  | 
 | ||||||
|  |         @JsonIgnore | ||||||
|  |         public final Collection<CourseQuiz> quizzes = new ArrayList<>(); | ||||||
|  | 
 | ||||||
|  |         @JsonCreator | ||||||
|  |         public CourseData( | ||||||
|  |                 @JsonProperty(value = "id") final String id, | ||||||
|  |                 @JsonProperty(value = "shortname") final String short_name, | ||||||
|  |                 @JsonProperty(value = "idnumber") final String idnumber, | ||||||
|  |                 @JsonProperty(value = "fullname") final String full_name, | ||||||
|  |                 @JsonProperty(value = "displayname") final String display_name, | ||||||
|  |                 @JsonProperty(value = "summary") final String summary, | ||||||
|  |                 @JsonProperty(value = "startdate") final Long start_date, | ||||||
|  |                 @JsonProperty(value = "enddate") final Long end_date, | ||||||
|  |                 @JsonProperty(value = "timecreated") final Long time_created, | ||||||
|  |                 @JsonProperty(value = "categoryid") final String category_id) { | ||||||
|  | 
 | ||||||
|  |             this.id = id; | ||||||
|  |             this.short_name = short_name; | ||||||
|  |             this.idnumber = idnumber; | ||||||
|  |             this.full_name = full_name; | ||||||
|  |             this.display_name = display_name; | ||||||
|  |             this.summary = summary; | ||||||
|  |             this.start_date = start_date; | ||||||
|  |             this.end_date = end_date; | ||||||
|  |             this.time_created = time_created; | ||||||
|  |             this.category_id = category_id; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @JsonIgnoreProperties(ignoreUnknown = true) | ||||||
|  |     public static final class Courses { | ||||||
|  |         public final Collection<CourseData> courses; | ||||||
|  |         public final Collection<Warning> warnings; | ||||||
|  | 
 | ||||||
|  |         @JsonCreator | ||||||
|  |         public Courses( | ||||||
|  |                 @JsonProperty(value = "courses") final Collection<CourseData> courses, | ||||||
|  |                 @JsonProperty(value = "warnings") final Collection<Warning> warnings) { | ||||||
|  |             this.courses = courses; | ||||||
|  |             this.warnings = warnings; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @JsonIgnoreProperties(ignoreUnknown = true) | ||||||
|  |     public static final class CourseQuizData { | ||||||
|  |         public final Collection<CourseQuiz> quizzes; | ||||||
|  |         public final Collection<Warning> warnings; | ||||||
|  | 
 | ||||||
|  |         @JsonCreator | ||||||
|  |         public CourseQuizData( | ||||||
|  |                 @JsonProperty(value = "quizzes") final Collection<CourseQuiz> quizzes, | ||||||
|  |                 @JsonProperty(value = "warnings") final Collection<Warning> warnings) { | ||||||
|  |             this.quizzes = quizzes; | ||||||
|  |             this.warnings = warnings; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @JsonIgnoreProperties(ignoreUnknown = true) | ||||||
|  |     public static final class CourseQuiz { | ||||||
|  |         public final String id; | ||||||
|  |         public final String course; | ||||||
|  |         public final String course_module; | ||||||
|  |         public final String name; | ||||||
|  |         public final String intro; // HTML | ||||||
|  |         public final Long time_open; // unix-time seconds UTC | ||||||
|  |         public final Long time_close; // unix-time seconds UTC | ||||||
|  |         public final Long time_limit; // unix-time seconds UTC | ||||||
|  | 
 | ||||||
|  |         @JsonCreator | ||||||
|  |         public CourseQuiz( | ||||||
|  |                 @JsonProperty(value = "id") final String id, | ||||||
|  |                 @JsonProperty(value = "course") final String course, | ||||||
|  |                 @JsonProperty(value = "coursemodule") final String course_module, | ||||||
|  |                 @JsonProperty(value = "name") final String name, | ||||||
|  |                 @JsonProperty(value = "intro") final String intro, | ||||||
|  |                 @JsonProperty(value = "timeopen") final Long time_open, | ||||||
|  |                 @JsonProperty(value = "timeclose") final Long time_close, | ||||||
|  |                 @JsonProperty(value = "timelimit") final Long time_limit) { | ||||||
|  | 
 | ||||||
|  |             this.id = id; | ||||||
|  |             this.course = course; | ||||||
|  |             this.course_module = course_module; | ||||||
|  |             this.name = name; | ||||||
|  |             this.intro = intro; | ||||||
|  |             this.time_open = time_open; | ||||||
|  |             this.time_close = time_close; | ||||||
|  |             this.time_limit = time_limit; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @JsonIgnoreProperties(ignoreUnknown = true) | ||||||
|  |     public static final class MoodleUserDetails { | ||||||
|  |         public final String id; | ||||||
|  |         public final String username; | ||||||
|  |         public final String firstname; | ||||||
|  |         public final String lastname; | ||||||
|  |         public final String fullname; | ||||||
|  |         public final String email; | ||||||
|  |         public final String department; | ||||||
|  |         public final Long firstaccess; | ||||||
|  |         public final Long lastaccess; | ||||||
|  |         public final String auth; | ||||||
|  |         public final Boolean suspended; | ||||||
|  |         public final Boolean confirmed; | ||||||
|  |         public final String lang; | ||||||
|  |         public final String theme; | ||||||
|  |         public final String timezone; | ||||||
|  |         public final String description; | ||||||
|  |         public final Integer mailformat; | ||||||
|  |         public final Integer descriptionformat; | ||||||
|  | 
 | ||||||
|  |         @JsonCreator | ||||||
|  |         public MoodleUserDetails( | ||||||
|  |                 @JsonProperty(value = "id") final String id, | ||||||
|  |                 @JsonProperty(value = "username") final String username, | ||||||
|  |                 @JsonProperty(value = "firstname") final String firstname, | ||||||
|  |                 @JsonProperty(value = "lastname") final String lastname, | ||||||
|  |                 @JsonProperty(value = "fullname") final String fullname, | ||||||
|  |                 @JsonProperty(value = "email") final String email, | ||||||
|  |                 @JsonProperty(value = "department") final String department, | ||||||
|  |                 @JsonProperty(value = "firstaccess") final Long firstaccess, | ||||||
|  |                 @JsonProperty(value = "lastaccess") final Long lastaccess, | ||||||
|  |                 @JsonProperty(value = "auth") final String auth, | ||||||
|  |                 @JsonProperty(value = "suspended") final Boolean suspended, | ||||||
|  |                 @JsonProperty(value = "confirmed") final Boolean confirmed, | ||||||
|  |                 @JsonProperty(value = "lang") final String lang, | ||||||
|  |                 @JsonProperty(value = "theme") final String theme, | ||||||
|  |                 @JsonProperty(value = "timezone") final String timezone, | ||||||
|  |                 @JsonProperty(value = "description") final String description, | ||||||
|  |                 @JsonProperty(value = "mailformat") final Integer mailformat, | ||||||
|  |                 @JsonProperty(value = "descriptionformat") final Integer descriptionformat) { | ||||||
|  | 
 | ||||||
|  |             this.id = id; | ||||||
|  |             this.username = username; | ||||||
|  |             this.firstname = firstname; | ||||||
|  |             this.lastname = lastname; | ||||||
|  |             this.fullname = fullname; | ||||||
|  |             this.email = email; | ||||||
|  |             this.department = department; | ||||||
|  |             this.firstaccess = firstaccess; | ||||||
|  |             this.lastaccess = lastaccess; | ||||||
|  |             this.auth = auth; | ||||||
|  |             this.suspended = suspended; | ||||||
|  |             this.confirmed = confirmed; | ||||||
|  |             this.lang = lang; | ||||||
|  |             this.theme = theme; | ||||||
|  |             this.timezone = timezone; | ||||||
|  |             this.description = description; | ||||||
|  |             this.mailformat = mailformat; | ||||||
|  |             this.descriptionformat = descriptionformat; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @JsonIgnoreProperties(ignoreUnknown = true) | ||||||
|  |     public static final class MoodlePluginUserDetails { | ||||||
|  |         public final String id; | ||||||
|  |         public final String fullname; | ||||||
|  |         public final String username; | ||||||
|  |         public final String firstname; | ||||||
|  |         public final String lastname; | ||||||
|  |         public final String idnumber; | ||||||
|  |         public final String email; | ||||||
|  |         public final Map<String, String> customfields; | ||||||
|  | 
 | ||||||
|  |         @JsonCreator | ||||||
|  |         public MoodlePluginUserDetails( | ||||||
|  |                 final String id, | ||||||
|  |                 final String username, | ||||||
|  |                 final String firstname, | ||||||
|  |                 final String lastname, | ||||||
|  |                 final String idnumber, | ||||||
|  |                 final String email, | ||||||
|  |                 final Map<String, String> customfields) { | ||||||
|  | 
 | ||||||
|  |             this.id = id; | ||||||
|  |             if (firstname != null && lastname != null) { | ||||||
|  |                 this.fullname = firstname + Constants.SPACE + lastname; | ||||||
|  |             } else if (firstname != null) { | ||||||
|  |                 this.fullname = firstname; | ||||||
|  |             } else { | ||||||
|  |                 this.fullname = lastname; | ||||||
|  |             } | ||||||
|  |             this.username = username; | ||||||
|  |             this.firstname = firstname; | ||||||
|  |             this.lastname = lastname; | ||||||
|  |             this.idnumber = idnumber; | ||||||
|  |             this.email = email; | ||||||
|  |             this.customfields = Utils.immutableMapOf(customfields); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @JsonIgnoreProperties(ignoreUnknown = true) | ||||||
|  |     public static final class CoursePage { | ||||||
|  |         public final Collection<CourseKey> courseKeys; | ||||||
|  |         public final Collection<Warning> warnings; | ||||||
|  | 
 | ||||||
|  |         public CoursePage( | ||||||
|  |                 @JsonProperty(value = "courses") final Collection<CourseKey> courseKeys, | ||||||
|  |                 @JsonProperty(value = "warnings") final Collection<Warning> warnings) { | ||||||
|  | 
 | ||||||
|  |             this.courseKeys = courseKeys; | ||||||
|  |             this.warnings = warnings; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @JsonIgnoreProperties(ignoreUnknown = true) | ||||||
|  |     public static final class CourseKey { | ||||||
|  |         public final String id; | ||||||
|  |         public final String short_name; | ||||||
|  |         public final String category_name; | ||||||
|  |         public final String sort_order; | ||||||
|  | 
 | ||||||
|  |         @JsonCreator | ||||||
|  |         public CourseKey( | ||||||
|  |                 @JsonProperty(value = "id") final String id, | ||||||
|  |                 @JsonProperty(value = "shortname") final String short_name, | ||||||
|  |                 @JsonProperty(value = "categoryname") final String category_name, | ||||||
|  |                 @JsonProperty(value = "sortorder") final String sort_order) { | ||||||
|  | 
 | ||||||
|  |             this.id = id; | ||||||
|  |             this.short_name = short_name; | ||||||
|  |             this.category_name = category_name; | ||||||
|  |             this.sort_order = sort_order; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         @Override | ||||||
|  |         public String toString() { | ||||||
|  |             final StringBuilder builder = new StringBuilder(); | ||||||
|  |             builder.append("CourseKey [id="); | ||||||
|  |             builder.append(this.id); | ||||||
|  |             builder.append(", short_name="); | ||||||
|  |             builder.append(this.short_name); | ||||||
|  |             builder.append(", category_name="); | ||||||
|  |             builder.append(this.category_name); | ||||||
|  |             builder.append(", sort_order="); | ||||||
|  |             builder.append(this.sort_order); | ||||||
|  |             builder.append("]"); | ||||||
|  |             return builder.toString(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @JsonIgnoreProperties(ignoreUnknown = true) | ||||||
|  |     public static final class MoodleQuizRestriction { | ||||||
|  |         public final String quiz_id; | ||||||
|  |         public final String config_keys; | ||||||
|  |         public final String browser_exam_keys; | ||||||
|  |         public final String quit_link; | ||||||
|  |         public final String quit_secret; | ||||||
|  | 
 | ||||||
|  |         @JsonCreator | ||||||
|  |         public MoodleQuizRestriction( | ||||||
|  |                 final String quiz_id, | ||||||
|  |                 final String config_keys, | ||||||
|  |                 final String browser_exam_keys, | ||||||
|  |                 final String quit_link, | ||||||
|  |                 final String quit_secret) { | ||||||
|  | 
 | ||||||
|  |             this.quiz_id = quiz_id; | ||||||
|  |             this.config_keys = config_keys; | ||||||
|  |             this.browser_exam_keys = browser_exam_keys; | ||||||
|  |             this.quit_link = quit_link; | ||||||
|  |             this.quit_secret = quit_secret; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -17,24 +17,18 @@ import java.util.List; | ||||||
| import java.util.Map; | import java.util.Map; | ||||||
| import java.util.Set; | import java.util.Set; | ||||||
| import java.util.function.Function; | import java.util.function.Function; | ||||||
| import java.util.function.Predicate; |  | ||||||
| import java.util.regex.Pattern; |  | ||||||
| import java.util.stream.Collectors; | import java.util.stream.Collectors; | ||||||
| import java.util.stream.Stream; | import java.util.stream.Stream; | ||||||
| 
 | 
 | ||||||
| import org.apache.commons.lang3.BooleanUtils; | import org.apache.commons.lang3.BooleanUtils; | ||||||
| import org.apache.commons.lang3.StringUtils; | import org.apache.commons.lang3.StringUtils; | ||||||
| import org.joda.time.DateTime; | import org.joda.time.DateTime; | ||||||
| import org.joda.time.DateTimeZone; |  | ||||||
| import org.slf4j.Logger; | import org.slf4j.Logger; | ||||||
| import org.slf4j.LoggerFactory; | import org.slf4j.LoggerFactory; | ||||||
| import org.springframework.core.env.Environment; | import org.springframework.core.env.Environment; | ||||||
| import org.springframework.util.LinkedMultiValueMap; | import org.springframework.util.LinkedMultiValueMap; | ||||||
| import org.springframework.util.MultiValueMap; | import org.springframework.util.MultiValueMap; | ||||||
| 
 | 
 | ||||||
| import com.fasterxml.jackson.annotation.JsonCreator; |  | ||||||
| import com.fasterxml.jackson.annotation.JsonIgnoreProperties; |  | ||||||
| import com.fasterxml.jackson.annotation.JsonProperty; |  | ||||||
| import com.fasterxml.jackson.core.JsonParseException; | import com.fasterxml.jackson.core.JsonParseException; | ||||||
| import com.fasterxml.jackson.core.type.TypeReference; | import com.fasterxml.jackson.core.type.TypeReference; | ||||||
| import com.fasterxml.jackson.databind.JsonMappingException; | import com.fasterxml.jackson.databind.JsonMappingException; | ||||||
|  | @ -56,8 +50,13 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.lms.CourseAccessAPI; | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.CourseAccessAPI; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService; | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleAPIRestTemplate; | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleAPIRestTemplate; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleAPIRestTemplate.Warning; |  | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory; | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory; | ||||||
|  | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils; | ||||||
|  | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils.CourseData; | ||||||
|  | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils.CoursePage; | ||||||
|  | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils.CourseQuizData; | ||||||
|  | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils.Courses; | ||||||
|  | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils.MoodleUserDetails; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.legacy.MoodleCourseDataAsyncLoader.CourseDataShort; | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.legacy.MoodleCourseDataAsyncLoader.CourseDataShort; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.legacy.MoodleCourseDataAsyncLoader.CourseQuizShort; | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.legacy.MoodleCourseDataAsyncLoader.CourseQuizShort; | ||||||
| 
 | 
 | ||||||
|  | @ -98,7 +97,7 @@ public class MoodleCourseAccess implements CourseAccessAPI { | ||||||
|     static final String MOODLE_COURSE_API_SEARCH_PAGE_SIZE = "perpage"; |     static final String MOODLE_COURSE_API_SEARCH_PAGE_SIZE = "perpage"; | ||||||
| 
 | 
 | ||||||
|     private final JSONMapper jsonMapper; |     private final JSONMapper jsonMapper; | ||||||
|     private final MoodleRestTemplateFactory moodleRestTemplateFactory; |     private final MoodleRestTemplateFactory restTemplateFactory; | ||||||
|     private final MoodleCourseDataAsyncLoader moodleCourseDataAsyncLoader; |     private final MoodleCourseDataAsyncLoader moodleCourseDataAsyncLoader; | ||||||
|     private final boolean prependShortCourseName; |     private final boolean prependShortCourseName; | ||||||
|     private final CircuitBreaker<String> protectedMoodlePageCall; |     private final CircuitBreaker<String> protectedMoodlePageCall; | ||||||
|  | @ -110,13 +109,13 @@ public class MoodleCourseAccess implements CourseAccessAPI { | ||||||
|     public MoodleCourseAccess( |     public MoodleCourseAccess( | ||||||
|             final JSONMapper jsonMapper, |             final JSONMapper jsonMapper, | ||||||
|             final AsyncService asyncService, |             final AsyncService asyncService, | ||||||
|             final MoodleRestTemplateFactory moodleRestTemplateFactory, |             final MoodleRestTemplateFactory restTemplateFactory, | ||||||
|             final MoodleCourseDataAsyncLoader moodleCourseDataAsyncLoader, |             final MoodleCourseDataAsyncLoader moodleCourseDataAsyncLoader, | ||||||
|             final Environment environment) { |             final Environment environment) { | ||||||
| 
 | 
 | ||||||
|         this.jsonMapper = jsonMapper; |         this.jsonMapper = jsonMapper; | ||||||
|         this.moodleCourseDataAsyncLoader = moodleCourseDataAsyncLoader; |         this.moodleCourseDataAsyncLoader = moodleCourseDataAsyncLoader; | ||||||
|         this.moodleRestTemplateFactory = moodleRestTemplateFactory; |         this.restTemplateFactory = restTemplateFactory; | ||||||
| 
 | 
 | ||||||
|         this.prependShortCourseName = BooleanUtils.toBoolean(environment.getProperty( |         this.prependShortCourseName = BooleanUtils.toBoolean(environment.getProperty( | ||||||
|                 "sebserver.webservice.lms.moodle.prependShortCourseName", |                 "sebserver.webservice.lms.moodle.prependShortCourseName", | ||||||
|  | @ -142,12 +141,12 @@ public class MoodleCourseAccess implements CourseAccessAPI { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     APITemplateDataSupplier getApiTemplateDataSupplier() { |     APITemplateDataSupplier getApiTemplateDataSupplier() { | ||||||
|         return this.moodleRestTemplateFactory.apiTemplateDataSupplier; |         return this.restTemplateFactory.getApiTemplateDataSupplier(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public LmsSetupTestResult testCourseAccessAPI() { |     public LmsSetupTestResult testCourseAccessAPI() { | ||||||
|         final LmsSetupTestResult attributesCheck = this.moodleRestTemplateFactory.test(); |         final LmsSetupTestResult attributesCheck = this.restTemplateFactory.test(); | ||||||
|         if (!attributesCheck.isOk()) { |         if (!attributesCheck.isOk()) { | ||||||
|             return attributesCheck; |             return attributesCheck; | ||||||
|         } |         } | ||||||
|  | @ -155,7 +154,7 @@ public class MoodleCourseAccess implements CourseAccessAPI { | ||||||
|         final Result<MoodleAPIRestTemplate> restTemplateRequest = getRestTemplate(); |         final Result<MoodleAPIRestTemplate> restTemplateRequest = getRestTemplate(); | ||||||
|         if (restTemplateRequest.hasError()) { |         if (restTemplateRequest.hasError()) { | ||||||
|             final String message = "Failed to gain access token from Moodle Rest API:\n tried token endpoints: " + |             final String message = "Failed to gain access token from Moodle Rest API:\n tried token endpoints: " + | ||||||
|                     this.moodleRestTemplateFactory.knownTokenAccessPaths; |                     this.restTemplateFactory.getKnownTokenAccessPaths(); | ||||||
|             log.error(message + " cause: {}", restTemplateRequest.getError().getMessage()); |             log.error(message + " cause: {}", restTemplateRequest.getError().getMessage()); | ||||||
|             return LmsSetupTestResult.ofTokenRequestError(LmsType.MOODLE, message); |             return LmsSetupTestResult.ofTokenRequestError(LmsType.MOODLE, message); | ||||||
|         } |         } | ||||||
|  | @ -189,12 +188,12 @@ public class MoodleCourseAccess implements CourseAccessAPI { | ||||||
|         try { |         try { | ||||||
|             int page = 0; |             int page = 0; | ||||||
|             final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup(); |             final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup(); | ||||||
|  |             final MoodleAPIRestTemplate restTemplate = getRestTemplate().getOrThrow(); | ||||||
|             final String urlPrefix = (lmsSetup.lmsApiUrl.endsWith(Constants.URL_PATH_SEPARATOR)) |             final String urlPrefix = (lmsSetup.lmsApiUrl.endsWith(Constants.URL_PATH_SEPARATOR)) | ||||||
|                     ? lmsSetup.lmsApiUrl + MOODLE_QUIZ_START_URL_PATH |                     ? lmsSetup.lmsApiUrl + MOODLE_QUIZ_START_URL_PATH | ||||||
|                     : lmsSetup.lmsApiUrl + Constants.URL_PATH_SEPARATOR + MOODLE_QUIZ_START_URL_PATH; |                     : lmsSetup.lmsApiUrl + Constants.URL_PATH_SEPARATOR + MOODLE_QUIZ_START_URL_PATH; | ||||||
| 
 | 
 | ||||||
|             while (!asyncQuizFetchBuffer.finished && !asyncQuizFetchBuffer.canceled) { |             while (!asyncQuizFetchBuffer.finished && !asyncQuizFetchBuffer.canceled) { | ||||||
|                 final MoodleAPIRestTemplate restTemplate = getRestTemplate().getOrThrow(); |  | ||||||
|                 // first get courses from Moodle for page |                 // first get courses from Moodle for page | ||||||
|                 final Map<String, CourseData> courseData = new HashMap<>(); |                 final Map<String, CourseData> courseData = new HashMap<>(); | ||||||
|                 final Collection<CourseData> coursesPage = getCoursesPage(restTemplate, page, this.pageSize); |                 final Collection<CourseData> coursesPage = getCoursesPage(restTemplate, page, this.pageSize); | ||||||
|  | @ -236,15 +235,10 @@ public class MoodleCourseAccess implements CourseAccessAPI { | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 if (courseQuizData.warnings != null && !courseQuizData.warnings.isEmpty()) { |                 if (courseQuizData.warnings != null && !courseQuizData.warnings.isEmpty()) { | ||||||
|                     log.warn( |                     MoodleUtils.logMoodleWarning( | ||||||
|                             "There are warnings from Moodle response: Moodle: {} request: {} warnings: {} warning sample: {}", |                             courseQuizData.warnings, | ||||||
|                             lmsSetup.name, |                             lmsSetup.name, | ||||||
|                             MoodleCourseAccess.MOODLE_QUIZ_API_FUNCTION_NAME, |                             MoodleCourseAccess.MOODLE_QUIZ_API_FUNCTION_NAME); | ||||||
|                             courseQuizData.warnings.size(), |  | ||||||
|                             courseQuizData.warnings.iterator().next().toString()); |  | ||||||
|                     if (log.isTraceEnabled()) { |  | ||||||
|                         log.trace("All warnings from Moodle: {}", courseQuizData.warnings.toString()); |  | ||||||
|                     } |  | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 if (courseQuizData.quizzes == null || courseQuizData.quizzes.isEmpty()) { |                 if (courseQuizData.quizzes == null || courseQuizData.quizzes.isEmpty()) { | ||||||
|  | @ -255,7 +249,7 @@ public class MoodleCourseAccess implements CourseAccessAPI { | ||||||
| 
 | 
 | ||||||
|                 courseQuizData.quizzes |                 courseQuizData.quizzes | ||||||
|                         .stream() |                         .stream() | ||||||
|                         .filter(getQuizFilter()) |                         .filter(MoodleUtils.getQuizFilter()) | ||||||
|                         .forEach(quiz -> { |                         .forEach(quiz -> { | ||||||
|                             final CourseData data = courseData.get(quiz.course); |                             final CourseData data = courseData.get(quiz.course); | ||||||
|                             if (data != null) { |                             if (data != null) { | ||||||
|  | @ -266,10 +260,16 @@ public class MoodleCourseAccess implements CourseAccessAPI { | ||||||
|                 courseData.values().stream() |                 courseData.values().stream() | ||||||
|                         .filter(c -> !c.quizzes.isEmpty()) |                         .filter(c -> !c.quizzes.isEmpty()) | ||||||
|                         .forEach(c -> asyncQuizFetchBuffer.buffer.addAll( |                         .forEach(c -> asyncQuizFetchBuffer.buffer.addAll( | ||||||
|                                 quizDataOf(lmsSetup, c, urlPrefix).stream() |                                 MoodleUtils.quizDataOf(lmsSetup, c, urlPrefix, this.prependShortCourseName) | ||||||
|  |                                         .stream() | ||||||
|                                         .filter(LmsAPIService.quizFilterPredicate(filterMap)) |                                         .filter(LmsAPIService.quizFilterPredicate(filterMap)) | ||||||
|                                         .collect(Collectors.toList()))); |                                         .collect(Collectors.toList()))); | ||||||
| 
 | 
 | ||||||
|  |                 if (asyncQuizFetchBuffer.buffer.size() > this.maxSize) { | ||||||
|  |                     log.warn("Maximal moodle quiz fetch size of {} reached. Cancel fetch at this point.", this.maxSize); | ||||||
|  |                     asyncQuizFetchBuffer.finish(); | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|                 page++; |                 page++; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|  | @ -298,8 +298,8 @@ public class MoodleCourseAccess implements CourseAccessAPI { | ||||||
|             final Map<String, CourseDataShort> cachedCourseData = this.moodleCourseDataAsyncLoader |             final Map<String, CourseDataShort> cachedCourseData = this.moodleCourseDataAsyncLoader | ||||||
|                     .getCachedCourseData(); |                     .getCachedCourseData(); | ||||||
| 
 | 
 | ||||||
|             final String courseId = getCourseId(id); |             final String courseId = MoodleUtils.getCourseId(id); | ||||||
|             final String quizId = getQuizId(id); |             final String quizId = MoodleUtils.getQuizId(id); | ||||||
|             if (cachedCourseData.containsKey(courseId)) { |             if (cachedCourseData.containsKey(courseId)) { | ||||||
|                 final CourseDataShort courseData = cachedCourseData.get(courseId); |                 final CourseDataShort courseData = cachedCourseData.get(courseId); | ||||||
|                 final CourseQuizShort quiz = courseData.quizzes |                 final CourseQuizShort quiz = courseData.quizzes | ||||||
|  | @ -352,7 +352,7 @@ public class MoodleCourseAccess implements CourseAccessAPI { | ||||||
|                     MOODLE_USER_PROFILE_API_FUNCTION_NAME, |                     MOODLE_USER_PROFILE_API_FUNCTION_NAME, | ||||||
|                     queryAttributes); |                     queryAttributes); | ||||||
| 
 | 
 | ||||||
|             if (checkAccessDeniedError(userDetailsJSON)) { |             if (MoodleUtils.checkAccessDeniedError(userDetailsJSON)) { | ||||||
|                 final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup(); |                 final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup(); | ||||||
|                 log.error("Get access denied error from Moodle: {} for API call: {}, response: {}", |                 log.error("Get access denied error from Moodle: {} for API call: {}, response: {}", | ||||||
|                         lmsSetup, |                         lmsSetup, | ||||||
|  | @ -486,7 +486,7 @@ public class MoodleCourseAccess implements CourseAccessAPI { | ||||||
|             final Map<String, CourseData> courseData = getCoursesForIds( |             final Map<String, CourseData> courseData = getCoursesForIds( | ||||||
|                     restTemplate, |                     restTemplate, | ||||||
|                     quizIds.stream() |                     quizIds.stream() | ||||||
|                             .map(MoodleCourseAccess::getCourseId) |                             .map(MoodleUtils::getCourseId) | ||||||
|                             .collect(Collectors.toSet())) |                             .collect(Collectors.toSet())) | ||||||
|                                     .stream() |                                     .stream() | ||||||
|                                     .collect(Collectors.toMap(cd -> cd.id, Function.identity())); |                                     .collect(Collectors.toMap(cd -> cd.id, Function.identity())); | ||||||
|  | @ -518,7 +518,12 @@ public class MoodleCourseAccess implements CourseAccessAPI { | ||||||
|                 return Collections.emptyList(); |                 return Collections.emptyList(); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             logMoodleWarnings(courseQuizData.warnings); |             if (courseQuizData.warnings != null && !courseQuizData.warnings.isEmpty()) { | ||||||
|  |                 MoodleUtils.logMoodleWarning( | ||||||
|  |                         courseQuizData.warnings, | ||||||
|  |                         lmsSetup.name, | ||||||
|  |                         MoodleCourseAccess.MOODLE_QUIZ_API_FUNCTION_NAME); | ||||||
|  |             } | ||||||
| 
 | 
 | ||||||
|             if (courseQuizData.quizzes == null || courseQuizData.quizzes.isEmpty()) { |             if (courseQuizData.quizzes == null || courseQuizData.quizzes.isEmpty()) { | ||||||
|                 log.error("No quizzes found for  ids: {} on LMS; {}", quizIds, lmsSetup.name); |                 log.error("No quizzes found for  ids: {} on LMS; {}", quizIds, lmsSetup.name); | ||||||
|  | @ -528,7 +533,7 @@ public class MoodleCourseAccess implements CourseAccessAPI { | ||||||
|             final Map<String, CourseData> finalCourseDataRef = courseData; |             final Map<String, CourseData> finalCourseDataRef = courseData; | ||||||
|             courseQuizData.quizzes |             courseQuizData.quizzes | ||||||
|                     .stream() |                     .stream() | ||||||
|                     .forEach(quiz -> fillSelectedQuizzes(quizIds, finalCourseDataRef, quiz)); |                     .forEach(quiz -> MoodleUtils.fillSelectedQuizzes(quizIds, finalCourseDataRef, quiz)); | ||||||
| 
 | 
 | ||||||
|             final String urlPrefix = (lmsSetup.lmsApiUrl.endsWith(Constants.URL_PATH_SEPARATOR)) |             final String urlPrefix = (lmsSetup.lmsApiUrl.endsWith(Constants.URL_PATH_SEPARATOR)) | ||||||
|                     ? lmsSetup.lmsApiUrl + MOODLE_QUIZ_START_URL_PATH |                     ? lmsSetup.lmsApiUrl + MOODLE_QUIZ_START_URL_PATH | ||||||
|  | @ -537,7 +542,11 @@ public class MoodleCourseAccess implements CourseAccessAPI { | ||||||
|             return courseData.values() |             return courseData.values() | ||||||
|                     .stream() |                     .stream() | ||||||
|                     .filter(c -> !c.quizzes.isEmpty()) |                     .filter(c -> !c.quizzes.isEmpty()) | ||||||
|                     .flatMap(cd -> quizDataOf(lmsSetup, cd, urlPrefix).stream()) |                     .flatMap(cd -> MoodleUtils.quizDataOf( | ||||||
|  |                             lmsSetup, | ||||||
|  |                             cd, | ||||||
|  |                             urlPrefix, | ||||||
|  |                             this.prependShortCourseName).stream()) | ||||||
|                     .collect(Collectors.toList()); |                     .collect(Collectors.toList()); | ||||||
| 
 | 
 | ||||||
|         } catch (final Exception e) { |         } catch (final Exception e) { | ||||||
|  | @ -546,27 +555,6 @@ public class MoodleCourseAccess implements CourseAccessAPI { | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private void fillSelectedQuizzes( |  | ||||||
|             final Set<String> quizIds, |  | ||||||
|             final Map<String, CourseData> finalCourseDataRef, |  | ||||||
|             final CourseQuiz quiz) { |  | ||||||
|         try { |  | ||||||
|             final CourseData course = finalCourseDataRef.get(quiz.course); |  | ||||||
|             if (course != null) { |  | ||||||
|                 final String internalQuizId = getInternalQuizId( |  | ||||||
|                         quiz.course_module, |  | ||||||
|                         course.id, |  | ||||||
|                         course.short_name, |  | ||||||
|                         course.idnumber); |  | ||||||
|                 if (quizIds.contains(internalQuizId)) { |  | ||||||
|                     course.quizzes.add(quiz); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } catch (final Exception e) { |  | ||||||
|             log.error("Failed to verify selected quiz for course: {}", e.getMessage()); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private Collection<CourseData> getCoursesForIds( |     private Collection<CourseData> getCoursesForIds( | ||||||
|             final MoodleAPIRestTemplate restTemplate, |             final MoodleAPIRestTemplate restTemplate, | ||||||
|             final Set<String> ids) { |             final Set<String> ids) { | ||||||
|  | @ -596,7 +584,12 @@ public class MoodleCourseAccess implements CourseAccessAPI { | ||||||
|                 return Collections.emptyList(); |                 return Collections.emptyList(); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             logMoodleWarnings(courses.warnings); |             if (courses.warnings != null && !courses.warnings.isEmpty()) { | ||||||
|  |                 MoodleUtils.logMoodleWarning( | ||||||
|  |                         courses.warnings, | ||||||
|  |                         lmsSetup.name, | ||||||
|  |                         MoodleCourseAccess.MOODLE_QUIZ_API_FUNCTION_NAME); | ||||||
|  |             } | ||||||
| 
 | 
 | ||||||
|             if (courses.courses == null || courses.courses.isEmpty()) { |             if (courses.courses == null || courses.courses.isEmpty()) { | ||||||
|                 log.error("No courses found for ids: {} on LMS: {}", ids, lmsSetup.name); |                 log.error("No courses found for ids: {} on LMS: {}", ids, lmsSetup.name); | ||||||
|  | @ -610,51 +603,6 @@ public class MoodleCourseAccess implements CourseAccessAPI { | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private List<QuizData> quizDataOf( |  | ||||||
|             final LmsSetup lmsSetup, |  | ||||||
|             final CourseData courseData, |  | ||||||
|             final String uriPrefix) { |  | ||||||
| 
 |  | ||||||
|         final Map<String, String> additionalAttrs = new HashMap<>(); |  | ||||||
|         additionalAttrs.put(QuizData.ATTR_ADDITIONAL_CREATION_TIME, String.valueOf(courseData.time_created)); |  | ||||||
|         additionalAttrs.put(QuizData.ATTR_ADDITIONAL_SHORT_NAME, courseData.short_name); |  | ||||||
|         additionalAttrs.put(QuizData.ATTR_ADDITIONAL_ID_NUMBER, courseData.idnumber); |  | ||||||
|         additionalAttrs.put(QuizData.ATTR_ADDITIONAL_FULL_NAME, courseData.full_name); |  | ||||||
|         additionalAttrs.put(QuizData.ATTR_ADDITIONAL_DISPLAY_NAME, courseData.display_name); |  | ||||||
|         additionalAttrs.put(QuizData.ATTR_ADDITIONAL_SUMMARY, courseData.summary); |  | ||||||
| 
 |  | ||||||
|         final List<QuizData> courseAndQuiz = courseData.quizzes |  | ||||||
|                 .stream() |  | ||||||
|                 .map(courseQuizData -> { |  | ||||||
|                     final String startURI = uriPrefix + courseQuizData.course_module; |  | ||||||
|                     additionalAttrs.put(QuizData.ATTR_ADDITIONAL_TIME_LIMIT, String.valueOf(courseQuizData.time_limit)); |  | ||||||
|                     return new QuizData( |  | ||||||
|                             getInternalQuizId( |  | ||||||
|                                     courseQuizData.course_module, |  | ||||||
|                                     courseData.id, |  | ||||||
|                                     courseData.short_name, |  | ||||||
|                                     courseData.idnumber), |  | ||||||
|                             lmsSetup.getInstitutionId(), |  | ||||||
|                             lmsSetup.id, |  | ||||||
|                             lmsSetup.getLmsType(), |  | ||||||
|                             (this.prependShortCourseName) |  | ||||||
|                                     ? courseData.short_name + " : " + courseQuizData.name |  | ||||||
|                                     : courseQuizData.name, |  | ||||||
|                             courseQuizData.intro, |  | ||||||
|                             (courseQuizData.time_open != null && courseQuizData.time_open > 0) |  | ||||||
|                                     ? Utils.toDateTimeUTCUnix(courseQuizData.time_open) |  | ||||||
|                                     : Utils.toDateTimeUTCUnix(courseData.start_date), |  | ||||||
|                             (courseQuizData.time_close != null && courseQuizData.time_close > 0) |  | ||||||
|                                     ? Utils.toDateTimeUTCUnix(courseQuizData.time_close) |  | ||||||
|                                     : Utils.toDateTimeUTCUnix(courseData.end_date), |  | ||||||
|                             startURI, |  | ||||||
|                             additionalAttrs); |  | ||||||
|                 }) |  | ||||||
|                 .collect(Collectors.toList()); |  | ||||||
| 
 |  | ||||||
|         return courseAndQuiz; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private List<QuizData> quizDataOf( |     private List<QuizData> quizDataOf( | ||||||
|             final LmsSetup lmsSetup, |             final LmsSetup lmsSetup, | ||||||
|             final CourseDataShort courseData, |             final CourseDataShort courseData, | ||||||
|  | @ -682,7 +630,7 @@ public class MoodleCourseAccess implements CourseAccessAPI { | ||||||
| 
 | 
 | ||||||
|         final String startURI = uriPrefix + courseQuizData.course_module; |         final String startURI = uriPrefix + courseQuizData.course_module; | ||||||
|         return new QuizData( |         return new QuizData( | ||||||
|                 getInternalQuizId( |                 MoodleUtils.getInternalQuizId( | ||||||
|                         courseQuizData.course_module, |                         courseQuizData.course_module, | ||||||
|                         courseData.id, |                         courseData.id, | ||||||
|                         courseData.short_name, |                         courseData.short_name, | ||||||
|  | @ -706,7 +654,7 @@ public class MoodleCourseAccess implements CourseAccessAPI { | ||||||
| 
 | 
 | ||||||
|     private Result<MoodleAPIRestTemplate> getRestTemplate() { |     private Result<MoodleAPIRestTemplate> getRestTemplate() { | ||||||
|         if (this.restTemplate == null) { |         if (this.restTemplate == null) { | ||||||
|             final Result<MoodleAPIRestTemplate> templateRequest = this.moodleRestTemplateFactory |             final Result<MoodleAPIRestTemplate> templateRequest = this.restTemplateFactory | ||||||
|                     .createRestTemplate(); |                     .createRestTemplate(); | ||||||
|             if (templateRequest.hasError()) { |             if (templateRequest.hasError()) { | ||||||
|                 return templateRequest; |                 return templateRequest; | ||||||
|  | @ -718,95 +666,6 @@ public class MoodleCourseAccess implements CourseAccessAPI { | ||||||
|         return Result.of(this.restTemplate); |         return Result.of(this.restTemplate); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static final String getInternalQuizId( |  | ||||||
|             final String quizId, |  | ||||||
|             final String courseId, |  | ||||||
|             final String shortname, |  | ||||||
|             final String idnumber) { |  | ||||||
| 
 |  | ||||||
|         return StringUtils.join( |  | ||||||
|                 new String[] { |  | ||||||
|                         quizId, |  | ||||||
|                         courseId, |  | ||||||
|                         StringUtils.isNotBlank(shortname) ? shortname : Constants.EMPTY_NOTE, |  | ||||||
|                         StringUtils.isNotBlank(idnumber) ? idnumber : Constants.EMPTY_NOTE |  | ||||||
|                 }, |  | ||||||
|                 Constants.COLON); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public static final String getQuizId(final String internalQuizId) { |  | ||||||
|         if (StringUtils.isBlank(internalQuizId)) { |  | ||||||
|             return null; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return StringUtils.split(internalQuizId, Constants.COLON)[0]; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public static final String getCourseId(final String internalQuizId) { |  | ||||||
|         if (StringUtils.isBlank(internalQuizId)) { |  | ||||||
|             return null; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return StringUtils.split(internalQuizId, Constants.COLON)[1]; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public static final String getShortname(final String internalQuizId) { |  | ||||||
|         if (StringUtils.isBlank(internalQuizId)) { |  | ||||||
|             return null; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         final String[] split = StringUtils.split(internalQuizId, Constants.COLON); |  | ||||||
|         if (split.length < 3) { |  | ||||||
|             return null; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         final String shortName = split[2]; |  | ||||||
|         return shortName.equals(Constants.EMPTY_NOTE) ? null : shortName; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public static final String getIdnumber(final String internalQuizId) { |  | ||||||
|         if (StringUtils.isBlank(internalQuizId)) { |  | ||||||
|             return null; |  | ||||||
|         } |  | ||||||
|         final String[] split = StringUtils.split(internalQuizId, Constants.COLON); |  | ||||||
|         if (split.length < 4) { |  | ||||||
|             return null; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         final String idNumber = split[3]; |  | ||||||
|         return idNumber.equals(Constants.EMPTY_NOTE) ? null : idNumber; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     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: {}", |  | ||||||
|                         lmsSetup, |  | ||||||
|                         MoodleCourseAccess.MOODLE_QUIZ_API_FUNCTION_NAME, |  | ||||||
|                         warnings.size(), |  | ||||||
|                         warnings.iterator().next().toString()); |  | ||||||
|             } else if (log.isTraceEnabled()) { |  | ||||||
|                 log.trace("All warnings from Moodle: {}", warnings.toString()); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private static final Pattern ACCESS_DENIED_PATTERN_1 = |  | ||||||
|             Pattern.compile(Pattern.quote("No access rights"), Pattern.CASE_INSENSITIVE); |  | ||||||
|     private static final Pattern ACCESS_DENIED_PATTERN_2 = |  | ||||||
|             Pattern.compile(Pattern.quote("access denied"), Pattern.CASE_INSENSITIVE); |  | ||||||
| 
 |  | ||||||
|     public static final boolean checkAccessDeniedError(final String courseKeyPageJSON) { |  | ||||||
|         return ACCESS_DENIED_PATTERN_1 |  | ||||||
|                 .matcher(courseKeyPageJSON) |  | ||||||
|                 .find() || |  | ||||||
|                 ACCESS_DENIED_PATTERN_2 |  | ||||||
|                         .matcher(courseKeyPageJSON) |  | ||||||
|                         .find(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private Collection<CourseData> getCoursesPage( |     private Collection<CourseData> getCoursesPage( | ||||||
|             final MoodleAPIRestTemplate restTemplate, |             final MoodleAPIRestTemplate restTemplate, | ||||||
|             final int page, |             final int page, | ||||||
|  | @ -866,7 +725,7 @@ public class MoodleCourseAccess implements CourseAccessAPI { | ||||||
| 
 | 
 | ||||||
|             final Collection<CourseData> result = getCoursesForIds(restTemplate, ids) |             final Collection<CourseData> result = getCoursesForIds(restTemplate, ids) | ||||||
|                     .stream() |                     .stream() | ||||||
|                     .filter(getCourseFilter()) |                     .filter(MoodleUtils.getCourseFilter()) | ||||||
|                     .collect(Collectors.toList()); |                     .collect(Collectors.toList()); | ||||||
| 
 | 
 | ||||||
|             if (log.isDebugEnabled()) { |             if (log.isDebugEnabled()) { | ||||||
|  | @ -882,258 +741,4 @@ public class MoodleCourseAccess implements CourseAccessAPI { | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private Predicate<CourseData> getCourseFilter() { |  | ||||||
|         final long now = Utils.getSecondsNow(); |  | ||||||
|         return course -> { |  | ||||||
|             if (course.start_date != null |  | ||||||
|                     && course.start_date < Utils.toUnixTimeInSeconds(DateTime.now(DateTimeZone.UTC).minusYears(3))) { |  | ||||||
|                 return false; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             if (course.end_date == null || course.end_date == 0 || course.end_date > now) { |  | ||||||
|                 return true; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             if (log.isDebugEnabled()) { |  | ||||||
|                 log.info("remove course {} end_time {} now {}", |  | ||||||
|                         course.short_name, |  | ||||||
|                         course.end_date, |  | ||||||
|                         now); |  | ||||||
|             } |  | ||||||
|             return false; |  | ||||||
|         }; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private Predicate<CourseQuiz> getQuizFilter() { |  | ||||||
|         final long now = Utils.getSecondsNow(); |  | ||||||
|         return quiz -> { |  | ||||||
|             if (quiz.time_close == null || quiz.time_close == 0 || quiz.time_close > now) { |  | ||||||
|                 return true; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             if (log.isDebugEnabled()) { |  | ||||||
|                 log.debug("remove quiz {} end_time {} now {}", |  | ||||||
|                         quiz.name, |  | ||||||
|                         quiz.time_close, |  | ||||||
|                         now); |  | ||||||
|             } |  | ||||||
|             return false; |  | ||||||
|         }; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // ---- Mapping Classes --- |  | ||||||
| 
 |  | ||||||
|     /** Maps the Moodle course API course data */ |  | ||||||
|     @JsonIgnoreProperties(ignoreUnknown = true) |  | ||||||
|     private static final class CourseData { |  | ||||||
|         final String id; |  | ||||||
|         final String short_name; |  | ||||||
|         final String idnumber; |  | ||||||
|         final String full_name; |  | ||||||
|         final String display_name; |  | ||||||
|         final String summary; |  | ||||||
|         final Long start_date; // unix-time seconds UTC |  | ||||||
|         final Long end_date; // unix-time seconds UTC |  | ||||||
|         final Long time_created; // unix-time seconds UTC |  | ||||||
|         final Collection<CourseQuiz> quizzes = new ArrayList<>(); |  | ||||||
| 
 |  | ||||||
|         @JsonCreator |  | ||||||
|         protected CourseData( |  | ||||||
|                 @JsonProperty(value = "id") final String id, |  | ||||||
|                 @JsonProperty(value = "shortname") final String short_name, |  | ||||||
|                 @JsonProperty(value = "idnumber") final String idnumber, |  | ||||||
|                 @JsonProperty(value = "fullname") final String full_name, |  | ||||||
|                 @JsonProperty(value = "displayname") final String display_name, |  | ||||||
|                 @JsonProperty(value = "summary") final String summary, |  | ||||||
|                 @JsonProperty(value = "startdate") final Long start_date, |  | ||||||
|                 @JsonProperty(value = "enddate") final Long end_date, |  | ||||||
|                 @JsonProperty(value = "timecreated") final Long time_created) { |  | ||||||
| 
 |  | ||||||
|             this.id = id; |  | ||||||
|             this.short_name = short_name; |  | ||||||
|             this.idnumber = idnumber; |  | ||||||
|             this.full_name = full_name; |  | ||||||
|             this.display_name = display_name; |  | ||||||
|             this.summary = summary; |  | ||||||
|             this.start_date = start_date; |  | ||||||
|             this.end_date = end_date; |  | ||||||
|             this.time_created = time_created; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @JsonIgnoreProperties(ignoreUnknown = true) |  | ||||||
|     private static final class Courses { |  | ||||||
|         final Collection<CourseData> courses; |  | ||||||
|         final Collection<Warning> warnings; |  | ||||||
| 
 |  | ||||||
|         @JsonCreator |  | ||||||
|         protected Courses( |  | ||||||
|                 @JsonProperty(value = "courses") final Collection<CourseData> courses, |  | ||||||
|                 @JsonProperty(value = "warnings") final Collection<Warning> warnings) { |  | ||||||
|             this.courses = courses; |  | ||||||
|             this.warnings = warnings; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @JsonIgnoreProperties(ignoreUnknown = true) |  | ||||||
|     private static final class CourseQuizData { |  | ||||||
|         final Collection<CourseQuiz> quizzes; |  | ||||||
|         final Collection<Warning> warnings; |  | ||||||
| 
 |  | ||||||
|         @JsonCreator |  | ||||||
|         protected CourseQuizData( |  | ||||||
|                 @JsonProperty(value = "quizzes") final Collection<CourseQuiz> quizzes, |  | ||||||
|                 @JsonProperty(value = "warnings") final Collection<Warning> warnings) { |  | ||||||
|             this.quizzes = quizzes; |  | ||||||
|             this.warnings = warnings; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @JsonIgnoreProperties(ignoreUnknown = true) |  | ||||||
|     static final class CourseQuiz { |  | ||||||
|         final String id; |  | ||||||
|         final String course; |  | ||||||
|         final String course_module; |  | ||||||
|         final String name; |  | ||||||
|         final String intro; // HTML |  | ||||||
|         final Long time_open; // unix-time seconds UTC |  | ||||||
|         final Long time_close; // unix-time seconds UTC |  | ||||||
|         final Long time_limit; // unix-time seconds UTC |  | ||||||
| 
 |  | ||||||
|         @JsonCreator |  | ||||||
|         protected CourseQuiz( |  | ||||||
|                 @JsonProperty(value = "id") final String id, |  | ||||||
|                 @JsonProperty(value = "course") final String course, |  | ||||||
|                 @JsonProperty(value = "coursemodule") final String course_module, |  | ||||||
|                 @JsonProperty(value = "name") final String name, |  | ||||||
|                 @JsonProperty(value = "intro") final String intro, |  | ||||||
|                 @JsonProperty(value = "timeopen") final Long time_open, |  | ||||||
|                 @JsonProperty(value = "timeclose") final Long time_close, |  | ||||||
|                 @JsonProperty(value = "timelimit") final Long time_limit) { |  | ||||||
| 
 |  | ||||||
|             this.id = id; |  | ||||||
|             this.course = course; |  | ||||||
|             this.course_module = course_module; |  | ||||||
|             this.name = name; |  | ||||||
|             this.intro = intro; |  | ||||||
|             this.time_open = time_open; |  | ||||||
|             this.time_close = time_close; |  | ||||||
|             this.time_limit = time_limit; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @JsonIgnoreProperties(ignoreUnknown = true) |  | ||||||
|     static final class MoodleUserDetails { |  | ||||||
|         final String id; |  | ||||||
|         final String username; |  | ||||||
|         final String firstname; |  | ||||||
|         final String lastname; |  | ||||||
|         final String fullname; |  | ||||||
|         final String email; |  | ||||||
|         final String department; |  | ||||||
|         final Long firstaccess; |  | ||||||
|         final Long lastaccess; |  | ||||||
|         final String auth; |  | ||||||
|         final Boolean suspended; |  | ||||||
|         final Boolean confirmed; |  | ||||||
|         final String lang; |  | ||||||
|         final String theme; |  | ||||||
|         final String timezone; |  | ||||||
|         final String description; |  | ||||||
|         final Integer mailformat; |  | ||||||
|         final Integer descriptionformat; |  | ||||||
| 
 |  | ||||||
|         @JsonCreator |  | ||||||
|         protected MoodleUserDetails( |  | ||||||
|                 @JsonProperty(value = "id") final String id, |  | ||||||
|                 @JsonProperty(value = "username") final String username, |  | ||||||
|                 @JsonProperty(value = "firstname") final String firstname, |  | ||||||
|                 @JsonProperty(value = "lastname") final String lastname, |  | ||||||
|                 @JsonProperty(value = "fullname") final String fullname, |  | ||||||
|                 @JsonProperty(value = "email") final String email, |  | ||||||
|                 @JsonProperty(value = "department") final String department, |  | ||||||
|                 @JsonProperty(value = "firstaccess") final Long firstaccess, |  | ||||||
|                 @JsonProperty(value = "lastaccess") final Long lastaccess, |  | ||||||
|                 @JsonProperty(value = "auth") final String auth, |  | ||||||
|                 @JsonProperty(value = "suspended") final Boolean suspended, |  | ||||||
|                 @JsonProperty(value = "confirmed") final Boolean confirmed, |  | ||||||
|                 @JsonProperty(value = "lang") final String lang, |  | ||||||
|                 @JsonProperty(value = "theme") final String theme, |  | ||||||
|                 @JsonProperty(value = "timezone") final String timezone, |  | ||||||
|                 @JsonProperty(value = "description") final String description, |  | ||||||
|                 @JsonProperty(value = "mailformat") final Integer mailformat, |  | ||||||
|                 @JsonProperty(value = "descriptionformat") final Integer descriptionformat) { |  | ||||||
| 
 |  | ||||||
|             this.id = id; |  | ||||||
|             this.username = username; |  | ||||||
|             this.firstname = firstname; |  | ||||||
|             this.lastname = lastname; |  | ||||||
|             this.fullname = fullname; |  | ||||||
|             this.email = email; |  | ||||||
|             this.department = department; |  | ||||||
|             this.firstaccess = firstaccess; |  | ||||||
|             this.lastaccess = lastaccess; |  | ||||||
|             this.auth = auth; |  | ||||||
|             this.suspended = suspended; |  | ||||||
|             this.confirmed = confirmed; |  | ||||||
|             this.lang = lang; |  | ||||||
|             this.theme = theme; |  | ||||||
|             this.timezone = timezone; |  | ||||||
|             this.description = description; |  | ||||||
|             this.mailformat = mailformat; |  | ||||||
|             this.descriptionformat = descriptionformat; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @JsonIgnoreProperties(ignoreUnknown = true) |  | ||||||
|     static final class CoursePage { |  | ||||||
|         final Collection<CourseKey> courseKeys; |  | ||||||
|         final Collection<Warning> warnings; |  | ||||||
| 
 |  | ||||||
|         public CoursePage( |  | ||||||
|                 @JsonProperty(value = "courses") final Collection<CourseKey> courseKeys, |  | ||||||
|                 @JsonProperty(value = "warnings") final Collection<Warning> warnings) { |  | ||||||
| 
 |  | ||||||
|             this.courseKeys = courseKeys; |  | ||||||
|             this.warnings = warnings; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @JsonIgnoreProperties(ignoreUnknown = true) |  | ||||||
|     static final class CourseKey { |  | ||||||
|         final String id; |  | ||||||
|         final String short_name; |  | ||||||
|         final String category_name; |  | ||||||
|         final String sort_order; |  | ||||||
| 
 |  | ||||||
|         @JsonCreator |  | ||||||
|         protected CourseKey( |  | ||||||
|                 @JsonProperty(value = "id") final String id, |  | ||||||
|                 @JsonProperty(value = "shortname") final String short_name, |  | ||||||
|                 @JsonProperty(value = "categoryname") final String category_name, |  | ||||||
|                 @JsonProperty(value = "sortorder") final String sort_order) { |  | ||||||
| 
 |  | ||||||
|             this.id = id; |  | ||||||
|             this.short_name = short_name; |  | ||||||
|             this.category_name = category_name; |  | ||||||
|             this.sort_order = sort_order; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         @Override |  | ||||||
|         public String toString() { |  | ||||||
|             final StringBuilder builder = new StringBuilder(); |  | ||||||
|             builder.append("CourseKey [id="); |  | ||||||
|             builder.append(this.id); |  | ||||||
|             builder.append(", short_name="); |  | ||||||
|             builder.append(this.short_name); |  | ||||||
|             builder.append(", category_name="); |  | ||||||
|             builder.append(this.category_name); |  | ||||||
|             builder.append(", sort_order="); |  | ||||||
|             builder.append(this.sort_order); |  | ||||||
|             builder.append("]"); |  | ||||||
|             return builder.toString(); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -49,7 +49,7 @@ import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; | ||||||
| import ch.ethz.seb.sebserver.gbl.util.Utils; | import ch.ethz.seb.sebserver.gbl.util.Utils; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleAPIRestTemplate; | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleAPIRestTemplate; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleAPIRestTemplate.Warning; | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleAPIRestTemplate.Warning; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.legacy.MoodleCourseAccess.CoursePage; | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils.CoursePage; | ||||||
| 
 | 
 | ||||||
| @Lazy | @Lazy | ||||||
| @Component | @Component | ||||||
|  |  | ||||||
|  | @ -8,408 +8,34 @@ | ||||||
| 
 | 
 | ||||||
| package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.legacy; | package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.legacy; | ||||||
| 
 | 
 | ||||||
| import java.util.ArrayList; |  | ||||||
| 
 |  | ||||||
| import org.apache.commons.lang3.StringUtils; |  | ||||||
| import org.slf4j.Logger; |  | ||||||
| import org.slf4j.LoggerFactory; |  | ||||||
| import org.springframework.util.LinkedMultiValueMap; |  | ||||||
| import org.springframework.util.MultiValueMap; |  | ||||||
| 
 |  | ||||||
| import com.fasterxml.jackson.annotation.JsonCreator; |  | ||||||
| import com.fasterxml.jackson.annotation.JsonIgnoreProperties; |  | ||||||
| import com.fasterxml.jackson.annotation.JsonProperty; |  | ||||||
| import com.fasterxml.jackson.core.type.TypeReference; |  | ||||||
| 
 |  | ||||||
| import ch.ethz.seb.sebserver.gbl.api.JSONMapper; |  | ||||||
| import ch.ethz.seb.sebserver.gbl.model.exam.Exam; | import ch.ethz.seb.sebserver.gbl.model.exam.Exam; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.exam.MoodleSEBRestriction; |  | ||||||
| import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction; | import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType; | import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult; | import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult; | ||||||
| import ch.ethz.seb.sebserver.gbl.util.Result; | import ch.ethz.seb.sebserver.gbl.util.Result; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionAPI; | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionAPI; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.NoSEBRestrictionException; |  | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleAPIRestTemplate; |  | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory; |  | ||||||
| 
 | 
 | ||||||
| /** GET: | /** Dummy Implementation */ | ||||||
|  * http://yourmoodle.org/webservice/rest/server.php?wstoken={token}&moodlewsrestformat=json&wsfunction=seb_restriction&courseId=123 |  | ||||||
|  * |  | ||||||
|  * Response (JSON): |  | ||||||
|  * |  | ||||||
|  * <pre> |  | ||||||
|  * { |  | ||||||
|  *   "quizId": "456", |  | ||||||
|  *   "configKeys": [ |  | ||||||
|  *       "key1", |  | ||||||
|  *       "key2", |  | ||||||
|  *       "key3" |  | ||||||
|  *   ], |  | ||||||
|  *   "browserKeys": [ |  | ||||||
|  *       "bkey1", |  | ||||||
|  *       "bkey2", |  | ||||||
|  *       "bkey3" |  | ||||||
|  *  ] |  | ||||||
|  * } |  | ||||||
|  * </pre> |  | ||||||
|  * |  | ||||||
|  * Set keys: |  | ||||||
|  * POST: |  | ||||||
|  * http://yourmoodle.org/webservice/rest/server.php?wstoken={token}&moodlewsrestformat=json&wsfunction=seb_restriction_update&courseId=123&configKey[0]=key1&configKey[1]=key2&browserKey[0]=bkey1&browserKey[1]=bkey2 |  | ||||||
|  * |  | ||||||
|  * Delete all key (and remove restrictions): |  | ||||||
|  * POST: |  | ||||||
|  * http://yourmoodle.org/webservice/rest/server.php?wstoken={token}&moodlewsrestformat=json&wsfunction=seb_restriction_delete&courseId=123 */ |  | ||||||
| public class MoodleCourseRestriction implements SEBRestrictionAPI { | public class MoodleCourseRestriction implements SEBRestrictionAPI { | ||||||
| 
 | 
 | ||||||
|     private static final Logger log = LoggerFactory.getLogger(MoodleCourseRestriction.class); |  | ||||||
| 
 |  | ||||||
|     private static final String MOODLE_DEFAULT_COURSE_RESTRICTION_WS_FUNCTION = "seb_restriction"; |  | ||||||
|     private static final String MOODLE_DEFAULT_COURSE_RESTRICTION_WS_FUNCTION_CREATE = "seb_restriction_create"; |  | ||||||
|     private static final String MOODLE_DEFAULT_COURSE_RESTRICTION_WS_FUNCTION_UPDATE = "seb_restriction_update"; |  | ||||||
|     private static final String MOODLE_DEFAULT_COURSE_RESTRICTION_WS_FUNCTION_DELETE = "seb_restriction_delete"; |  | ||||||
|     private static final String MOODLE_DEFAULT_COURSE_RESTRICTION_SHORT_NAME = "shortname"; |  | ||||||
|     private static final String MOODLE_DEFAULT_COURSE_RESTRICTION_ID_NUMBER = "idnumber"; |  | ||||||
|     private static final String MOODLE_DEFAULT_COURSE_RESTRICTION_QUIZ_ID = "quizId"; |  | ||||||
|     private static final String MOODLE_DEFAULT_COURSE_RESTRICTION_CONFIG_KEY = "configKey"; |  | ||||||
|     private static final String MOODLE_DEFAULT_COURSE_RESTRICTION_BROWSER_KEY = "browserKey"; |  | ||||||
| 
 |  | ||||||
|     private final JSONMapper jsonMapper; |  | ||||||
|     private final MoodleRestTemplateFactory moodleRestTemplateFactory; |  | ||||||
| 
 |  | ||||||
|     private MoodleAPIRestTemplate restTemplate; |  | ||||||
| 
 |  | ||||||
|     public MoodleCourseRestriction( |  | ||||||
|             final JSONMapper jsonMapper, |  | ||||||
|             final MoodleRestTemplateFactory moodleRestTemplateFactory) { |  | ||||||
| 
 |  | ||||||
|         this.jsonMapper = jsonMapper; |  | ||||||
|         this.moodleRestTemplateFactory = moodleRestTemplateFactory; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |     @Override | ||||||
|     public LmsSetupTestResult testCourseRestrictionAPI() { |     public LmsSetupTestResult testCourseRestrictionAPI() { | ||||||
|         // try to call the SEB Restrictions API |         return LmsSetupTestResult.ofQuizAccessAPIError(LmsType.MOODLE, "SEB restriction not supported"); | ||||||
|         try { |  | ||||||
| 
 |  | ||||||
|             final MoodleAPIRestTemplate template = getRestTemplate() |  | ||||||
|                     .getOrThrow(); |  | ||||||
| 
 |  | ||||||
|             final String jsonResponse = template.callMoodleAPIFunction( |  | ||||||
|                     MOODLE_DEFAULT_COURSE_RESTRICTION_WS_FUNCTION, |  | ||||||
|                     new LinkedMultiValueMap<>(), |  | ||||||
|                     null); |  | ||||||
| 
 |  | ||||||
|             final Error checkError = this.checkError(jsonResponse); |  | ||||||
|             if (checkError != null) { |  | ||||||
|                 return LmsSetupTestResult.ofQuizRestrictionAPIError(LmsType.MOODLE, checkError.exception); |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|         } catch (final Exception e) { |  | ||||||
|             log.debug("Moodle SEB restriction API not available: ", e); |  | ||||||
|             return LmsSetupTestResult.ofQuizRestrictionAPIError(LmsType.MOODLE, e.getMessage()); |  | ||||||
|         } |  | ||||||
|         return LmsSetupTestResult.ofOkay(LmsType.MOODLE); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public Result<SEBRestriction> getSEBClientRestriction(final Exam exam) { |     public Result<SEBRestriction> getSEBClientRestriction(final Exam exam) { | ||||||
|         return Result.tryCatch(() -> { |         return Result.ofError(new UnsupportedOperationException("SEB restriction not supported")); | ||||||
|             return getSEBRestriction( |  | ||||||
|                     MoodleCourseAccess.getQuizId(exam.externalId), |  | ||||||
|                     MoodleCourseAccess.getShortname(exam.externalId), |  | ||||||
|                     MoodleCourseAccess.getIdnumber(exam.externalId)) |  | ||||||
|                             .map(restriction -> SEBRestriction.from(exam.id, restriction)) |  | ||||||
|                             .getOrThrow(); |  | ||||||
|         }); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public Result<SEBRestriction> applySEBClientRestriction( |     public Result<SEBRestriction> applySEBClientRestriction(final Exam exam, final SEBRestriction sebRestrictionData) { | ||||||
|             final Exam exam, |         return Result.ofError(new UnsupportedOperationException("SEB restriction not supported")); | ||||||
|             final SEBRestriction sebRestrictionData) { |  | ||||||
| 
 |  | ||||||
|         return this.updateSEBRestriction( |  | ||||||
|                 exam.externalId, |  | ||||||
|                 MoodleSEBRestriction.from(sebRestrictionData)) |  | ||||||
|                 .map(result -> sebRestrictionData); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public Result<Exam> releaseSEBClientRestriction(final Exam exam) { |     public Result<Exam> releaseSEBClientRestriction(final Exam exam) { | ||||||
|         return this.deleteSEBRestriction(exam.externalId) |         return Result.ofError(new UnsupportedOperationException("SEB restriction not supported")); | ||||||
|                 .map(result -> exam); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     Result<MoodleSEBRestriction> getSEBRestriction( |  | ||||||
|             final String quizId, |  | ||||||
|             final String shortname, |  | ||||||
|             final String idnumber) { |  | ||||||
| 
 |  | ||||||
|         if (log.isDebugEnabled()) { |  | ||||||
|             log.debug("GET SEB Client restriction on course: {} quiz: {}", shortname, quizId); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return Result.tryCatch(() -> { |  | ||||||
| 
 |  | ||||||
|             final MoodleAPIRestTemplate template = getRestTemplate() |  | ||||||
|                     .getOrThrow(); |  | ||||||
| 
 |  | ||||||
|             final MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>(); |  | ||||||
|             queryParams.add(MOODLE_DEFAULT_COURSE_RESTRICTION_QUIZ_ID, quizId); |  | ||||||
|             if (StringUtils.isNotBlank(shortname)) { |  | ||||||
|                 queryParams.add(MOODLE_DEFAULT_COURSE_RESTRICTION_SHORT_NAME, shortname); |  | ||||||
|             } |  | ||||||
|             if (StringUtils.isNotBlank(idnumber)) { |  | ||||||
|                 queryParams.add(MOODLE_DEFAULT_COURSE_RESTRICTION_ID_NUMBER, idnumber); |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             final String resultJSON = template.callMoodleAPIFunction( |  | ||||||
|                     MOODLE_DEFAULT_COURSE_RESTRICTION_WS_FUNCTION, |  | ||||||
|                     queryParams, |  | ||||||
|                     null); |  | ||||||
| 
 |  | ||||||
|             final Error error = this.checkError(resultJSON); |  | ||||||
|             if (error != null) { |  | ||||||
|                 log.error("Failed to get SEB restriction: {}", error.toString()); |  | ||||||
|                 throw new NoSEBRestrictionException("Failed to get SEB restriction: " + error.exception); |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             final MoodleSEBRestriction restrictiondata = this.jsonMapper.readValue( |  | ||||||
|                     resultJSON, |  | ||||||
|                     new TypeReference<MoodleSEBRestriction>() { |  | ||||||
|                     }); |  | ||||||
| 
 |  | ||||||
|             return restrictiondata; |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     Result<MoodleSEBRestriction> createSEBRestriction( |  | ||||||
|             final String internalId, |  | ||||||
|             final MoodleSEBRestriction restriction) { |  | ||||||
| 
 |  | ||||||
|         return Result.tryCatch(() -> { |  | ||||||
|             return createSEBRestriction( |  | ||||||
|                     MoodleCourseAccess.getQuizId(internalId), |  | ||||||
|                     MoodleCourseAccess.getShortname(internalId), |  | ||||||
|                     MoodleCourseAccess.getIdnumber(internalId), |  | ||||||
|                     restriction) |  | ||||||
|                             .getOrThrow(); |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     Result<MoodleSEBRestriction> createSEBRestriction( |  | ||||||
|             final String quizId, |  | ||||||
|             final String shortname, |  | ||||||
|             final String idnumber, |  | ||||||
|             final MoodleSEBRestriction restriction) { |  | ||||||
| 
 |  | ||||||
|         if (log.isDebugEnabled()) { |  | ||||||
|             log.debug("POST SEB Client restriction on course: {} quiz: restriction : {}", |  | ||||||
|                     shortname, |  | ||||||
|                     quizId, |  | ||||||
|                     restriction); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return postSEBRestriction( |  | ||||||
|                 quizId, |  | ||||||
|                 shortname, |  | ||||||
|                 idnumber, |  | ||||||
|                 MOODLE_DEFAULT_COURSE_RESTRICTION_WS_FUNCTION_CREATE, |  | ||||||
|                 restriction); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     Result<MoodleSEBRestriction> updateSEBRestriction( |  | ||||||
|             final String internalId, |  | ||||||
|             final MoodleSEBRestriction restriction) { |  | ||||||
| 
 |  | ||||||
|         return Result.tryCatch(() -> { |  | ||||||
|             return updateSEBRestriction( |  | ||||||
|                     MoodleCourseAccess.getQuizId(internalId), |  | ||||||
|                     MoodleCourseAccess.getShortname(internalId), |  | ||||||
|                     MoodleCourseAccess.getIdnumber(internalId), |  | ||||||
|                     restriction) |  | ||||||
|                             .getOrThrow(); |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     Result<MoodleSEBRestriction> updateSEBRestriction( |  | ||||||
|             final String quizId, |  | ||||||
|             final String shortname, |  | ||||||
|             final String idnumber, |  | ||||||
|             final MoodleSEBRestriction restriction) { |  | ||||||
| 
 |  | ||||||
|         if (log.isDebugEnabled()) { |  | ||||||
|             log.debug("POST SEB Client restriction on course: {} quiz: restriction : {}", |  | ||||||
|                     shortname, |  | ||||||
|                     quizId, |  | ||||||
|                     restriction); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return postSEBRestriction( |  | ||||||
|                 quizId, |  | ||||||
|                 shortname, |  | ||||||
|                 idnumber, |  | ||||||
|                 MOODLE_DEFAULT_COURSE_RESTRICTION_WS_FUNCTION_UPDATE, |  | ||||||
|                 restriction); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     Result<Boolean> deleteSEBRestriction( |  | ||||||
|             final String internalId) { |  | ||||||
| 
 |  | ||||||
|         return Result.tryCatch(() -> { |  | ||||||
|             return deleteSEBRestriction( |  | ||||||
|                     MoodleCourseAccess.getQuizId(internalId), |  | ||||||
|                     MoodleCourseAccess.getShortname(internalId), |  | ||||||
|                     MoodleCourseAccess.getIdnumber(internalId)) |  | ||||||
|                             .getOrThrow(); |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     Result<Boolean> deleteSEBRestriction( |  | ||||||
|             final String quizId, |  | ||||||
|             final String shortname, |  | ||||||
|             final String idnumber) { |  | ||||||
| 
 |  | ||||||
|         if (log.isDebugEnabled()) { |  | ||||||
|             log.debug("DELETE SEB Client restriction on course: {} quizId {}", shortname, quizId); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return Result.tryCatch(() -> { |  | ||||||
|             final MoodleAPIRestTemplate template = getRestTemplate() |  | ||||||
|                     .getOrThrow(); |  | ||||||
| 
 |  | ||||||
|             final MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>(); |  | ||||||
|             queryParams.add(MOODLE_DEFAULT_COURSE_RESTRICTION_QUIZ_ID, quizId); |  | ||||||
|             if (StringUtils.isNotBlank(shortname)) { |  | ||||||
|                 queryParams.add(MOODLE_DEFAULT_COURSE_RESTRICTION_SHORT_NAME, shortname); |  | ||||||
|             } |  | ||||||
|             if (StringUtils.isNotBlank(idnumber)) { |  | ||||||
|                 queryParams.add(MOODLE_DEFAULT_COURSE_RESTRICTION_ID_NUMBER, idnumber); |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             final String jsonResponse = template.callMoodleAPIFunction( |  | ||||||
|                     MOODLE_DEFAULT_COURSE_RESTRICTION_WS_FUNCTION_DELETE, |  | ||||||
|                     queryParams, |  | ||||||
|                     null); |  | ||||||
| 
 |  | ||||||
|             final Error error = this.checkError(jsonResponse); |  | ||||||
|             if (error != null) { |  | ||||||
|                 log.error("Failed to delete SEB restriction: {}", error.toString()); |  | ||||||
|                 return false; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             return true; |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private Result<MoodleAPIRestTemplate> getRestTemplate() { |  | ||||||
|         if (this.restTemplate == null) { |  | ||||||
|             final Result<MoodleAPIRestTemplate> templateRequest = this.moodleRestTemplateFactory |  | ||||||
|                     .createRestTemplate(); |  | ||||||
|             if (templateRequest.hasError()) { |  | ||||||
|                 return templateRequest; |  | ||||||
|             } else { |  | ||||||
|                 this.restTemplate = templateRequest.get(); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return Result.of(this.restTemplate); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private Result<MoodleSEBRestriction> postSEBRestriction( |  | ||||||
|             final String quizId, |  | ||||||
|             final String shortname, |  | ||||||
|             final String idnumber, |  | ||||||
|             final String function, |  | ||||||
|             final MoodleSEBRestriction restriction) { |  | ||||||
|         return Result.tryCatch(() -> { |  | ||||||
| 
 |  | ||||||
|             final MoodleAPIRestTemplate template = getRestTemplate() |  | ||||||
|                     .getOrThrow(); |  | ||||||
| 
 |  | ||||||
|             final MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>(); |  | ||||||
|             queryParams.add(MOODLE_DEFAULT_COURSE_RESTRICTION_QUIZ_ID, quizId); |  | ||||||
|             if (StringUtils.isNotBlank(shortname)) { |  | ||||||
|                 queryParams.add(MOODLE_DEFAULT_COURSE_RESTRICTION_SHORT_NAME, shortname); |  | ||||||
|             } |  | ||||||
|             if (StringUtils.isNotBlank(idnumber)) { |  | ||||||
|                 queryParams.add(MOODLE_DEFAULT_COURSE_RESTRICTION_ID_NUMBER, idnumber); |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             final MultiValueMap<String, String> queryAttributes = new LinkedMultiValueMap<>(); |  | ||||||
|             queryAttributes.addAll( |  | ||||||
|                     MOODLE_DEFAULT_COURSE_RESTRICTION_CONFIG_KEY, |  | ||||||
|                     new ArrayList<>(restriction.configKeys)); |  | ||||||
|             queryAttributes.addAll( |  | ||||||
|                     MOODLE_DEFAULT_COURSE_RESTRICTION_BROWSER_KEY, |  | ||||||
|                     new ArrayList<>(restriction.browserExamKeys)); |  | ||||||
| 
 |  | ||||||
|             final String resultJSON = template.callMoodleAPIFunction( |  | ||||||
|                     function, |  | ||||||
|                     queryParams, |  | ||||||
|                     queryAttributes); |  | ||||||
| 
 |  | ||||||
|             final Error error = this.checkError(resultJSON); |  | ||||||
|             if (error != null) { |  | ||||||
|                 log.error("Failed to post SEB restriction: {}", error.toString()); |  | ||||||
|                 throw new NoSEBRestrictionException("Failed to post SEB restriction: " + error.exception); |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             final MoodleSEBRestriction restrictiondata = this.jsonMapper.readValue( |  | ||||||
|                     resultJSON, |  | ||||||
|                     new TypeReference<MoodleSEBRestriction>() { |  | ||||||
|                     }); |  | ||||||
| 
 |  | ||||||
|             return restrictiondata; |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public Error checkError(final String jsonResponse) { |  | ||||||
|         if (jsonResponse.contains("exception") || jsonResponse.contains("errorcode")) { |  | ||||||
|             try { |  | ||||||
|                 return this.jsonMapper.readValue( |  | ||||||
|                         jsonResponse, |  | ||||||
|                         new TypeReference<Error>() { |  | ||||||
|                         }); |  | ||||||
|             } catch (final Exception e) { |  | ||||||
|                 log.error("Failed to parse error response: {} cause: ", jsonResponse, e); |  | ||||||
|                 return null; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return null; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @JsonIgnoreProperties(ignoreUnknown = true) |  | ||||||
|     private static class Error { |  | ||||||
|         public final String exception; |  | ||||||
|         public final String errorcode; |  | ||||||
|         public final String message; |  | ||||||
| 
 |  | ||||||
|         @JsonCreator |  | ||||||
|         Error( |  | ||||||
|                 @JsonProperty(value = "exception") final String exception, |  | ||||||
|                 @JsonProperty(value = "errorcode") final String errorcode, |  | ||||||
|                 @JsonProperty(value = "message") final String message) { |  | ||||||
|             this.exception = exception; |  | ||||||
|             this.errorcode = errorcode; |  | ||||||
|             this.message = message; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         @Override |  | ||||||
|         public String toString() { |  | ||||||
|             final StringBuilder builder = new StringBuilder(); |  | ||||||
|             builder.append("Error [exception="); |  | ||||||
|             builder.append(this.exception); |  | ||||||
|             builder.append(", errorcode="); |  | ||||||
|             builder.append(this.errorcode); |  | ||||||
|             builder.append(", message="); |  | ||||||
|             builder.append(this.message); |  | ||||||
|             builder.append("]"); |  | ||||||
|             return builder.toString(); |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -25,12 +25,14 @@ import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType; | import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType; | ||||||
| import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; | import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; | ||||||
| import ch.ethz.seb.sebserver.gbl.util.Result; | import ch.ethz.seb.sebserver.gbl.util.Result; | ||||||
|  | import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamConfigurationValueService; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier; | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate; | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplateFactory; | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplateFactory; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.LmsAPITemplateAdapter; | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.LmsAPITemplateAdapter; | ||||||
|  | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodlePluginCheck; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory; | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.plugin.MoodlePluginCheck; | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactoryImpl; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.plugin.MoodlePluginCourseAccess; | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.plugin.MoodlePluginCourseAccess; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.plugin.MoodlePluginCourseRestriction; | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.plugin.MoodlePluginCourseRestriction; | ||||||
| 
 | 
 | ||||||
|  | @ -46,6 +48,7 @@ public class MoodleLmsAPITemplateFactory implements LmsAPITemplateFactory { | ||||||
|     private final Environment environment; |     private final Environment environment; | ||||||
|     private final ClientCredentialService clientCredentialService; |     private final ClientCredentialService clientCredentialService; | ||||||
|     private final ClientHttpRequestFactoryService clientHttpRequestFactoryService; |     private final ClientHttpRequestFactoryService clientHttpRequestFactoryService; | ||||||
|  |     private final ExamConfigurationValueService examConfigurationValueService; | ||||||
|     private final ApplicationContext applicationContext; |     private final ApplicationContext applicationContext; | ||||||
|     private final String[] alternativeTokenRequestPaths; |     private final String[] alternativeTokenRequestPaths; | ||||||
| 
 | 
 | ||||||
|  | @ -56,6 +59,7 @@ public class MoodleLmsAPITemplateFactory implements LmsAPITemplateFactory { | ||||||
|             final AsyncService asyncService, |             final AsyncService asyncService, | ||||||
|             final Environment environment, |             final Environment environment, | ||||||
|             final ClientCredentialService clientCredentialService, |             final ClientCredentialService clientCredentialService, | ||||||
|  |             final ExamConfigurationValueService examConfigurationValueService, | ||||||
|             final ClientHttpRequestFactoryService clientHttpRequestFactoryService, |             final ClientHttpRequestFactoryService clientHttpRequestFactoryService, | ||||||
|             final ApplicationContext applicationContext, |             final ApplicationContext applicationContext, | ||||||
|             @Value("${sebserver.webservice.lms.moodle.api.token.request.paths:}") final String alternativeTokenRequestPaths) { |             @Value("${sebserver.webservice.lms.moodle.api.token.request.paths:}") final String alternativeTokenRequestPaths) { | ||||||
|  | @ -66,6 +70,7 @@ public class MoodleLmsAPITemplateFactory implements LmsAPITemplateFactory { | ||||||
|         this.asyncService = asyncService; |         this.asyncService = asyncService; | ||||||
|         this.environment = environment; |         this.environment = environment; | ||||||
|         this.clientCredentialService = clientCredentialService; |         this.clientCredentialService = clientCredentialService; | ||||||
|  |         this.examConfigurationValueService = examConfigurationValueService; | ||||||
|         this.clientHttpRequestFactoryService = clientHttpRequestFactoryService; |         this.clientHttpRequestFactoryService = clientHttpRequestFactoryService; | ||||||
|         this.applicationContext = applicationContext; |         this.applicationContext = applicationContext; | ||||||
|         this.alternativeTokenRequestPaths = (alternativeTokenRequestPaths != null) |         this.alternativeTokenRequestPaths = (alternativeTokenRequestPaths != null) | ||||||
|  | @ -88,20 +93,26 @@ public class MoodleLmsAPITemplateFactory implements LmsAPITemplateFactory { | ||||||
|                     .getBean(MoodleCourseDataAsyncLoader.class); |                     .getBean(MoodleCourseDataAsyncLoader.class); | ||||||
|             asyncLoaderPrototype.init(lmsSetup.getModelId()); |             asyncLoaderPrototype.init(lmsSetup.getModelId()); | ||||||
| 
 | 
 | ||||||
|             final MoodleRestTemplateFactory moodleRestTemplateFactory = new MoodleRestTemplateFactory( |             final MoodleRestTemplateFactory restTemplateFactory = new MoodleRestTemplateFactoryImpl( | ||||||
|                     this.jsonMapper, |                     this.jsonMapper, | ||||||
|                     apiTemplateDataSupplier, |                     apiTemplateDataSupplier, | ||||||
|                     this.clientCredentialService, |                     this.clientCredentialService, | ||||||
|                     this.clientHttpRequestFactoryService, |                     this.clientHttpRequestFactoryService, | ||||||
|                     this.alternativeTokenRequestPaths); |                     this.alternativeTokenRequestPaths); | ||||||
| 
 | 
 | ||||||
|             if (this.moodlePluginCheck.checkPluginAvailable(lmsSetup)) { |             if (this.moodlePluginCheck.checkPluginAvailable(restTemplateFactory)) { | ||||||
| 
 | 
 | ||||||
|                 final MoodlePluginCourseAccess moodlePluginCourseAccess = new MoodlePluginCourseAccess( |                 final MoodlePluginCourseAccess moodlePluginCourseAccess = new MoodlePluginCourseAccess( | ||||||
|                         this.jsonMapper, |                         this.jsonMapper, | ||||||
|                         moodleRestTemplateFactory, |                         this.asyncService, | ||||||
|                         this.cacheManager); |                         restTemplateFactory, | ||||||
|                 final MoodlePluginCourseRestriction moodlePluginCourseRestriction = new MoodlePluginCourseRestriction(); |                         this.cacheManager, | ||||||
|  |                         this.environment); | ||||||
|  | 
 | ||||||
|  |                 final MoodlePluginCourseRestriction moodlePluginCourseRestriction = new MoodlePluginCourseRestriction( | ||||||
|  |                         this.jsonMapper, | ||||||
|  |                         restTemplateFactory, | ||||||
|  |                         this.examConfigurationValueService); | ||||||
| 
 | 
 | ||||||
|                 return new LmsAPITemplateAdapter( |                 return new LmsAPITemplateAdapter( | ||||||
|                         this.asyncService, |                         this.asyncService, | ||||||
|  | @ -115,20 +126,16 @@ public class MoodleLmsAPITemplateFactory implements LmsAPITemplateFactory { | ||||||
|                 final MoodleCourseAccess moodleCourseAccess = new MoodleCourseAccess( |                 final MoodleCourseAccess moodleCourseAccess = new MoodleCourseAccess( | ||||||
|                         this.jsonMapper, |                         this.jsonMapper, | ||||||
|                         this.asyncService, |                         this.asyncService, | ||||||
|                         moodleRestTemplateFactory, |                         restTemplateFactory, | ||||||
|                         asyncLoaderPrototype, |                         asyncLoaderPrototype, | ||||||
|                         this.environment); |                         this.environment); | ||||||
| 
 | 
 | ||||||
|                 final MoodleCourseRestriction moodleCourseRestriction = new MoodleCourseRestriction( |  | ||||||
|                         this.jsonMapper, |  | ||||||
|                         moodleRestTemplateFactory); |  | ||||||
| 
 |  | ||||||
|                 return new LmsAPITemplateAdapter( |                 return new LmsAPITemplateAdapter( | ||||||
|                         this.asyncService, |                         this.asyncService, | ||||||
|                         this.environment, |                         this.environment, | ||||||
|                         apiTemplateDataSupplier, |                         apiTemplateDataSupplier, | ||||||
|                         moodleCourseAccess, |                         moodleCourseAccess, | ||||||
|                         moodleCourseRestriction); |                         new MoodleCourseRestriction()); | ||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -1,31 +0,0 @@ | ||||||
| /* |  | ||||||
|  * Copyright (c) 2022 ETH Zürich, Educational Development and Technology (LET) |  | ||||||
|  * |  | ||||||
|  * This Source Code Form is subject to the terms of the Mozilla Public |  | ||||||
|  * License, v. 2.0. If a copy of the MPL was not distributed with this |  | ||||||
|  * file, You can obtain one at http://mozilla.org/MPL/2.0/. |  | ||||||
|  */ |  | ||||||
| 
 |  | ||||||
| package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.plugin; |  | ||||||
| 
 |  | ||||||
| import org.springframework.context.annotation.Lazy; |  | ||||||
| import org.springframework.stereotype.Service; |  | ||||||
| 
 |  | ||||||
| import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; |  | ||||||
| import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; |  | ||||||
| 
 |  | ||||||
| @Lazy |  | ||||||
| @Service |  | ||||||
| @WebServiceProfile |  | ||||||
| public class MoodlePluginCheck { |  | ||||||
| 
 |  | ||||||
|     /** Used to check if the moodle SEB Server plugin is available for a given LMSSetup. |  | ||||||
|      * |  | ||||||
|      * @param lmsSetup The LMS Setup |  | ||||||
|      * @return true if the SEB Server plugin is available */ |  | ||||||
|     public boolean checkPluginAvailable(final LmsSetup lmsSetup) { |  | ||||||
|         // TODO check if the moodle plugin is installed for the specified LMS Setup |  | ||||||
|         return false; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| } |  | ||||||
|  | @ -8,64 +8,127 @@ | ||||||
| 
 | 
 | ||||||
| package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.plugin; | package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.plugin; | ||||||
| 
 | 
 | ||||||
|  | import java.io.IOException; | ||||||
| import java.util.ArrayList; | import java.util.ArrayList; | ||||||
| import java.util.Collection; | import java.util.Collection; | ||||||
| import java.util.Collections; | import java.util.Collections; | ||||||
|  | import java.util.HashMap; | ||||||
|  | import java.util.HashSet; | ||||||
|  | import java.util.Iterator; | ||||||
| import java.util.List; | import java.util.List; | ||||||
|  | import java.util.Map; | ||||||
|  | import java.util.Objects; | ||||||
| import java.util.Set; | import java.util.Set; | ||||||
|  | import java.util.function.Function; | ||||||
|  | import java.util.function.Predicate; | ||||||
|  | import java.util.stream.Collectors; | ||||||
|  | import java.util.stream.Stream; | ||||||
| 
 | 
 | ||||||
|  | import org.apache.commons.lang3.BooleanUtils; | ||||||
|  | import org.apache.commons.lang3.StringUtils; | ||||||
|  | import org.joda.time.DateTime; | ||||||
| import org.slf4j.Logger; | import org.slf4j.Logger; | ||||||
| import org.slf4j.LoggerFactory; | import org.slf4j.LoggerFactory; | ||||||
| import org.springframework.cache.CacheManager; | import org.springframework.cache.CacheManager; | ||||||
|  | import org.springframework.core.env.Environment; | ||||||
|  | import org.springframework.util.LinkedMultiValueMap; | ||||||
|  | import org.springframework.util.MultiValueMap; | ||||||
| 
 | 
 | ||||||
| import com.fasterxml.jackson.annotation.JsonCreator; | import com.fasterxml.jackson.core.JsonParseException; | ||||||
| import com.fasterxml.jackson.annotation.JsonIgnoreProperties; | import com.fasterxml.jackson.core.type.TypeReference; | ||||||
| import com.fasterxml.jackson.annotation.JsonProperty; | import com.fasterxml.jackson.databind.JsonMappingException; | ||||||
| 
 | 
 | ||||||
|  | import ch.ethz.seb.sebserver.gbl.Constants; | ||||||
| import ch.ethz.seb.sebserver.gbl.api.JSONMapper; | import ch.ethz.seb.sebserver.gbl.api.JSONMapper; | ||||||
|  | import ch.ethz.seb.sebserver.gbl.async.AsyncService; | ||||||
|  | import ch.ethz.seb.sebserver.gbl.async.CircuitBreaker; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.exam.Chapters; | import ch.ethz.seb.sebserver.gbl.model.exam.Chapters; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.exam.QuizData; | import ch.ethz.seb.sebserver.gbl.model.exam.QuizData; | ||||||
|  | import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType; | import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult; | import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails; | import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails; | ||||||
| import ch.ethz.seb.sebserver.gbl.util.Result; | import ch.ethz.seb.sebserver.gbl.util.Result; | ||||||
|  | import ch.ethz.seb.sebserver.gbl.util.Utils; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; | import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.lms.CourseAccessAPI; | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.CourseAccessAPI; | ||||||
|  | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.AbstractCachedCourseAccess; | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.AbstractCachedCourseAccess; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleAPIRestTemplate; | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleAPIRestTemplate; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleAPIRestTemplate.Warning; |  | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory; | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory; | ||||||
|  | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils; | ||||||
|  | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils.CourseData; | ||||||
|  | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils.CourseQuizData; | ||||||
|  | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils.Courses; | ||||||
|  | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils.MoodlePluginUserDetails; | ||||||
| 
 | 
 | ||||||
| public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess implements CourseAccessAPI { | public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess implements CourseAccessAPI { | ||||||
| 
 | 
 | ||||||
|     private static final Logger log = LoggerFactory.getLogger(MoodlePluginCourseAccess.class); |     private static final Logger log = LoggerFactory.getLogger(MoodlePluginCourseAccess.class); | ||||||
| 
 | 
 | ||||||
|     static final String COURSES_API_FUNCTION_NAME = "local_sebserver_get_courses"; |     public static final String MOODLE_QUIZ_START_URL_PATH = "mod/quiz/view.php?id="; | ||||||
|     static final String QUIZZES_BY_COURSES_API_FUNCTION_NAME = "local_sebserver_get_quizzes_by_courses"; |     public static final String COURSES_API_FUNCTION_NAME = "quizaccess_sebserver_get_courses"; | ||||||
|     static final String USERS_API_FUNCTION_NAME = "local_sebserver_get_users"; |     public static final String QUIZZES_BY_COURSES_API_FUNCTION_NAME = "quizaccess_sebserver_get_quizzes_by_courses"; | ||||||
|  |     public static final String USERS_API_FUNCTION_NAME = "quizaccess_sebserver_get_users"; | ||||||
| 
 | 
 | ||||||
|     static final String CRITERIA_FROM_DATE = "from_date"; |     public static final String ATTR_FIELD = "field"; | ||||||
|     static final String CRITERIA_TO_DATE = "to_date"; |     public static final String CRITERIA_COURSE_IDS = "ids"; | ||||||
|     static final String CRITERIA_LIMIT_FROM = "limitfrom"; |     public static final String CRITERIA_FROM_DATE = "from_date"; | ||||||
|     static final String CRITERIA_LIMIT_NUM = "limitnum"; |     public static final String CRITERIA_TO_DATE = "to_date"; | ||||||
|  |     public static final String CRITERIA_LIMIT_FROM = "limitfrom"; | ||||||
|  |     public static final String CRITERIA_LIMIT_NUM = "limitnum"; | ||||||
| 
 | 
 | ||||||
|     private final JSONMapper jsonMapper; |     private final JSONMapper jsonMapper; | ||||||
|     private final MoodleRestTemplateFactory moodleRestTemplateFactory; |     private final MoodleRestTemplateFactory restTemplateFactory; | ||||||
|  |     private final CircuitBreaker<String> protectedMoodlePageCall; | ||||||
|  |     private final boolean prependShortCourseName; | ||||||
|  |     private final int pageSize; | ||||||
|  |     private final int maxSize; | ||||||
| 
 | 
 | ||||||
|     private MoodleAPIRestTemplate restTemplate; |     private MoodleAPIRestTemplate restTemplate; | ||||||
| 
 | 
 | ||||||
|     public MoodlePluginCourseAccess( |     public MoodlePluginCourseAccess( | ||||||
|             final JSONMapper jsonMapper, |             final JSONMapper jsonMapper, | ||||||
|             final MoodleRestTemplateFactory moodleRestTemplateFactory, |             final AsyncService asyncService, | ||||||
|             final CacheManager cacheManager) { |             final MoodleRestTemplateFactory restTemplateFactory, | ||||||
|  |             final CacheManager cacheManager, | ||||||
|  |             final Environment environment) { | ||||||
|  | 
 | ||||||
|         super(cacheManager); |         super(cacheManager); | ||||||
|         this.jsonMapper = jsonMapper; |         this.jsonMapper = jsonMapper; | ||||||
|         this.moodleRestTemplateFactory = moodleRestTemplateFactory; |         this.restTemplateFactory = restTemplateFactory; | ||||||
|  | 
 | ||||||
|  |         this.prependShortCourseName = BooleanUtils.toBoolean(environment.getProperty( | ||||||
|  |                 "sebserver.webservice.lms.moodle.prependShortCourseName", | ||||||
|  |                 Constants.TRUE_STRING)); | ||||||
|  | 
 | ||||||
|  |         this.protectedMoodlePageCall = asyncService.createCircuitBreaker( | ||||||
|  |                 environment.getProperty( | ||||||
|  |                         "sebserver.webservice.circuitbreaker.moodleRestCall.attempts", | ||||||
|  |                         Integer.class, | ||||||
|  |                         2), | ||||||
|  |                 environment.getProperty( | ||||||
|  |                         "sebserver.webservice.circuitbreaker.moodleRestCall.blockingTime", | ||||||
|  |                         Long.class, | ||||||
|  |                         Constants.SECOND_IN_MILLIS * 20), | ||||||
|  |                 environment.getProperty( | ||||||
|  |                         "sebserver.webservice.circuitbreaker.moodleRestCall.timeToRecover", | ||||||
|  |                         Long.class, | ||||||
|  |                         Constants.MINUTE_IN_MILLIS)); | ||||||
|  |         this.maxSize = | ||||||
|  |                 environment.getProperty("sebserver.webservice.cache.moodle.course.maxSize", Integer.class, 10000); | ||||||
|  |         this.pageSize = | ||||||
|  |                 environment.getProperty("sebserver.webservice.cache.moodle.course.pageSize", Integer.class, 10); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     protected Long getLmsSetupId() { | ||||||
|  |         return this.restTemplateFactory.getApiTemplateDataSupplier().getLmsSetup().id; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public LmsSetupTestResult testCourseAccessAPI() { |     public LmsSetupTestResult testCourseAccessAPI() { | ||||||
|         final LmsSetupTestResult attributesCheck = this.moodleRestTemplateFactory.test(); |         final LmsSetupTestResult attributesCheck = this.restTemplateFactory.test(); | ||||||
|         if (!attributesCheck.isOk()) { |         if (!attributesCheck.isOk()) { | ||||||
|             return attributesCheck; |             return attributesCheck; | ||||||
|         } |         } | ||||||
|  | @ -73,78 +136,417 @@ public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess impleme | ||||||
|         final Result<MoodleAPIRestTemplate> restTemplateRequest = getRestTemplate(); |         final Result<MoodleAPIRestTemplate> restTemplateRequest = getRestTemplate(); | ||||||
|         if (restTemplateRequest.hasError()) { |         if (restTemplateRequest.hasError()) { | ||||||
|             final String message = "Failed to gain access token from Moodle Rest API:\n tried token endpoints: " + |             final String message = "Failed to gain access token from Moodle Rest API:\n tried token endpoints: " + | ||||||
|                     this.moodleRestTemplateFactory.knownTokenAccessPaths; |                     this.restTemplateFactory.getKnownTokenAccessPaths(); | ||||||
|             log.error(message + " cause: {}", restTemplateRequest.getError().getMessage()); |             log.error(message + " cause: {}", restTemplateRequest.getError().getMessage()); | ||||||
|             return LmsSetupTestResult.ofTokenRequestError(LmsType.MOODLE_PLUGIN, message); |             return LmsSetupTestResult.ofTokenRequestError(LmsType.MOODLE_PLUGIN, message); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         final MoodleAPIRestTemplate restTemplate = restTemplateRequest.get(); |         final MoodleAPIRestTemplate restTemplate = restTemplateRequest.get(); | ||||||
| 
 | 
 | ||||||
| //        try { |         try { | ||||||
| //            restTemplate.testAPIConnection( | 
 | ||||||
| //                    COURSES_API_FUNCTION_NAME, |             restTemplate.testAPIConnection( | ||||||
| //                    QUIZZES_BY_COURSES_API_FUNCTION_NAME, |                     COURSES_API_FUNCTION_NAME, | ||||||
| //                    USERS_API_FUNCTION_NAME); |                     QUIZZES_BY_COURSES_API_FUNCTION_NAME, | ||||||
| //        } catch (final RuntimeException e) { |                     USERS_API_FUNCTION_NAME); | ||||||
| //            log.error("Failed to access Moodle course API: ", e); | 
 | ||||||
| //            return LmsSetupTestResult.ofQuizAccessAPIError(LmsType.MOODLE_PLUGIN, e.getMessage()); |         } catch (final RuntimeException e) { | ||||||
| //        } |             log.error("Failed to access Moodle course API: ", e); | ||||||
|  |             return LmsSetupTestResult.ofQuizAccessAPIError(LmsType.MOODLE_PLUGIN, e.getMessage()); | ||||||
|  |         } | ||||||
| 
 | 
 | ||||||
|         return LmsSetupTestResult.ofOkay(LmsType.MOODLE_PLUGIN); |         return LmsSetupTestResult.ofOkay(LmsType.MOODLE_PLUGIN); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public Result<List<QuizData>> getQuizzes(final FilterMap filterMap) { |     public Result<List<QuizData>> getQuizzes(final FilterMap filterMap) { | ||||||
|         System.out.println("***************** filterMap: " + filterMap); |         return Result.ofError(new UnsupportedOperationException()); | ||||||
|         // TODO Auto-generated method stub |  | ||||||
|         return Result.of(Collections.emptyList()); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public Result<Collection<QuizData>> getQuizzes(final Set<String> ids) { |  | ||||||
|         // TODO Auto-generated method stub |  | ||||||
|         return null; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public void fetchQuizzes(final FilterMap filterMap, final AsyncQuizFetchBuffer asyncQuizFetchBuffer) { |     public void fetchQuizzes(final FilterMap filterMap, final AsyncQuizFetchBuffer asyncQuizFetchBuffer) { | ||||||
|         // TODO Auto-generated method stub |         try { | ||||||
| 
 | 
 | ||||||
|  |             int page = 0; | ||||||
|  |             int failedAttempts = 0; | ||||||
|  |             final DateTime quizFromTime = filterMap.getQuizFromTime(); | ||||||
|  |             final Predicate<QuizData> quizFilter = LmsAPIService.quizFilterPredicate(filterMap); | ||||||
|  | 
 | ||||||
|  |             while (!asyncQuizFetchBuffer.finished && !asyncQuizFetchBuffer.canceled) { | ||||||
|  |                 try { | ||||||
|  |                     fetchQuizzesPage(page, quizFromTime, asyncQuizFetchBuffer, quizFilter); | ||||||
|  |                     page++; | ||||||
|  |                 } catch (final Exception e) { | ||||||
|  |                     log.error("Unexpected error while trying to fetch moodle quiz page: {}", page, e); | ||||||
|  |                     failedAttempts++; | ||||||
|  |                     if (failedAttempts > 3) { | ||||||
|  |                         asyncQuizFetchBuffer.finish(e); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |         } catch (final Exception e) { | ||||||
|  |             asyncQuizFetchBuffer.finish(e); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public Result<Collection<QuizData>> getQuizzes(final Set<String> ids) { | ||||||
|  |         return Result.tryCatch(() -> { | ||||||
|  | 
 | ||||||
|  |             final Set<String> missingIds = new HashSet<>(ids); | ||||||
|  |             final Collection<QuizData> result = new ArrayList<>(); | ||||||
|  |             final Set<String> fromCache = ids.stream() | ||||||
|  |                     .map(super::getFromCache).filter(Objects::nonNull) | ||||||
|  |                     .map(qd -> { | ||||||
|  |                         result.add(qd); | ||||||
|  |                         return qd.id; | ||||||
|  |                     }).collect(Collectors.toSet()); | ||||||
|  |             missingIds.removeAll(fromCache); | ||||||
|  | 
 | ||||||
|  |             if (!missingIds.isEmpty()) { | ||||||
|  | 
 | ||||||
|  |                 result.addAll(getRestTemplate() | ||||||
|  |                         .map(template -> getQuizzesForIds(template, ids)) | ||||||
|  |                         .map(super::putToCache) | ||||||
|  |                         .onError(error -> log.error("Failed to get courses for: {}", ids, error)) | ||||||
|  |                         .getOrElse(() -> Collections.emptyList())); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             return result; | ||||||
|  |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public Result<QuizData> getQuiz(final String id) { |     public Result<QuizData> getQuiz(final String id) { | ||||||
|         // TODO Auto-generated method stub |         return Result.tryCatch(() -> { | ||||||
|         return null; | 
 | ||||||
|  |             final QuizData fromCache = super.getFromCache(id); | ||||||
|  |             if (fromCache != null) { | ||||||
|  |                 return fromCache; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             final Set<String> ids = Stream.of(id).collect(Collectors.toSet()); | ||||||
|  |             final Iterator<QuizData> iterator = getRestTemplate() | ||||||
|  |                     .map(template -> getQuizzesForIds(template, ids)) | ||||||
|  |                     .map(super::putToCache) | ||||||
|  |                     .getOr(Collections.emptyList()) | ||||||
|  |                     .iterator(); | ||||||
|  | 
 | ||||||
|  |             if (!iterator.hasNext()) { | ||||||
|  |                 throw new RuntimeException("Moodle Quiz for id " + id + " not found"); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             return iterator.next(); | ||||||
|  |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeUserId) { |     public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeUserId) { | ||||||
|         // TODO Auto-generated method stub |         return Result.tryCatch(() -> { | ||||||
|         return null; | 
 | ||||||
|  |             final MoodleAPIRestTemplate template = getRestTemplate() | ||||||
|  |                     .getOrThrow(); | ||||||
|  | 
 | ||||||
|  |             final MultiValueMap<String, String> queryAttributes = new LinkedMultiValueMap<>(); | ||||||
|  |             queryAttributes.add(ATTR_FIELD, "id"); | ||||||
|  |             queryAttributes.add("values[0]", examineeUserId); | ||||||
|  | 
 | ||||||
|  |             final String userDetailsJSON = template.callMoodleAPIFunction( | ||||||
|  |                     USERS_API_FUNCTION_NAME, | ||||||
|  |                     queryAttributes); | ||||||
|  | 
 | ||||||
|  |             if (MoodleUtils.checkAccessDeniedError(userDetailsJSON)) { | ||||||
|  |                 final LmsSetup lmsSetup = this.restTemplateFactory.getApiTemplateDataSupplier().getLmsSetup(); | ||||||
|  |                 log.error("Get access denied error from Moodle: {} for API call: {}, response: {}", | ||||||
|  |                         lmsSetup, | ||||||
|  |                         USERS_API_FUNCTION_NAME, | ||||||
|  |                         Utils.truncateText(userDetailsJSON, 2000)); | ||||||
|  |                 throw new RuntimeException("No user details on Moodle API request (access-denied)"); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             final MoodlePluginUserDetails[] userDetails = this.jsonMapper.<MoodlePluginUserDetails[]> readValue( | ||||||
|  |                     userDetailsJSON, | ||||||
|  |                     new TypeReference<MoodlePluginUserDetails[]>() { | ||||||
|  |                     }); | ||||||
|  | 
 | ||||||
|  |             if (userDetails == null || userDetails.length <= 0) { | ||||||
|  |                 throw new RuntimeException("No user details on Moodle API request"); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             return new ExamineeAccountDetails( | ||||||
|  |                     userDetails[0].id, | ||||||
|  |                     userDetails[0].fullname, | ||||||
|  |                     userDetails[0].username, | ||||||
|  |                     userDetails[0].email, | ||||||
|  |                     userDetails[0].customfields); | ||||||
|  |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public String getExamineeName(final String examineeUserId) { |     public String getExamineeName(final String examineeUserId) { | ||||||
|         // TODO Auto-generated method stub |         return getExamineeAccountDetails(examineeUserId) | ||||||
|         return null; |                 .map(ExamineeAccountDetails::getDisplayName) | ||||||
|  |                 .onError(error -> log.warn("Failed to request user-name for ID: {}", error.getMessage(), error)) | ||||||
|  |                 .getOr(examineeUserId); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public Result<Chapters> getCourseChapters(final String courseId) { |     public Result<Chapters> getCourseChapters(final String courseId) { | ||||||
|         // TODO Auto-generated method stub |         return Result.of(new Chapters(Collections.emptyList())); | ||||||
|         return null; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     private String getLmsSetupName() { | ||||||
|     protected Long getLmsSetupId() { |         return this.restTemplateFactory.getApiTemplateDataSupplier().getLmsSetup().name; | ||||||
|         // TODO Auto-generated method stub |     } | ||||||
|         return null; | 
 | ||||||
|  |     private void fetchQuizzesPage( | ||||||
|  |             final int page, | ||||||
|  |             final DateTime quizFromTime, | ||||||
|  |             final AsyncQuizFetchBuffer asyncQuizFetchBuffer, | ||||||
|  |             final Predicate<QuizData> quizFilter) throws JsonParseException, JsonMappingException, IOException { | ||||||
|  | 
 | ||||||
|  |         final MoodleAPIRestTemplate restTemplate = getRestTemplate().getOrThrow(); | ||||||
|  | 
 | ||||||
|  |         final LmsSetup lmsSetup = this.restTemplateFactory.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; | ||||||
|  | 
 | ||||||
|  |         // first get courses page from moodle | ||||||
|  |         final Map<String, CourseData> courseData = new HashMap<>(); | ||||||
|  |         final Collection<CourseData> coursesPage = getCoursesPage(restTemplate, quizFromTime, page, this.pageSize); | ||||||
|  | 
 | ||||||
|  |         // no courses for page --> finish | ||||||
|  |         if (coursesPage == null || coursesPage.isEmpty()) { | ||||||
|  |             asyncQuizFetchBuffer.finish(); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         courseData.putAll(coursesPage | ||||||
|  |                 .stream() | ||||||
|  |                 .collect(Collectors.toMap( | ||||||
|  |                         cd -> cd.id, | ||||||
|  |                         Function.identity()))); | ||||||
|  | 
 | ||||||
|  |         // then get all quizzes of courses and filter | ||||||
|  |         final LinkedMultiValueMap<String, String> attributes = new LinkedMultiValueMap<>(); | ||||||
|  |         final List<String> courseIds = new ArrayList<>(courseData.keySet()); | ||||||
|  |         attributes.put(CRITERIA_COURSE_IDS, courseIds); | ||||||
|  | 
 | ||||||
|  |         final String quizzesJSON = this.protectedMoodlePageCall | ||||||
|  |                 .protectedRun(() -> restTemplate.callMoodleAPIFunction( | ||||||
|  |                         QUIZZES_BY_COURSES_API_FUNCTION_NAME, | ||||||
|  |                         attributes)) | ||||||
|  |                 .getOrThrow(); | ||||||
|  | 
 | ||||||
|  |         final CourseQuizData courseQuizData = this.jsonMapper.readValue( | ||||||
|  |                 quizzesJSON, | ||||||
|  |                 CourseQuizData.class); | ||||||
|  | 
 | ||||||
|  |         if (courseQuizData == null) { | ||||||
|  |             return; // SEBSERV-361 | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (courseQuizData.warnings != null && !courseQuizData.warnings.isEmpty()) { | ||||||
|  |             MoodleUtils.logMoodleWarning( | ||||||
|  |                     courseQuizData.warnings, | ||||||
|  |                     lmsSetup.name, | ||||||
|  |                     QUIZZES_BY_COURSES_API_FUNCTION_NAME); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (courseQuizData.quizzes == null || courseQuizData.quizzes.isEmpty()) { | ||||||
|  |             return; // no quizzes on this page | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         courseQuizData.quizzes | ||||||
|  |                 .stream() | ||||||
|  |                 .filter(MoodleUtils.getQuizFilter()) | ||||||
|  |                 .forEach(quiz -> { | ||||||
|  |                     final CourseData data = courseData.get(quiz.course); | ||||||
|  |                     if (data != null) { | ||||||
|  |                         data.quizzes.add(quiz); | ||||||
|  |                     } | ||||||
|  |                 }); | ||||||
|  | 
 | ||||||
|  |         courseData.values().stream() | ||||||
|  |                 .filter(c -> !c.quizzes.isEmpty()) | ||||||
|  |                 .forEach(c -> asyncQuizFetchBuffer.buffer.addAll( | ||||||
|  |                         MoodleUtils.quizDataOf(lmsSetup, c, urlPrefix, this.prependShortCourseName) | ||||||
|  |                                 .stream() | ||||||
|  |                                 .filter(quizFilter) | ||||||
|  |                                 .collect(Collectors.toList()))); | ||||||
|  | 
 | ||||||
|  |         if (asyncQuizFetchBuffer.buffer.size() > this.maxSize) { | ||||||
|  |             log.warn("Maximal moodle quiz fetch size of {} reached. Cancel fetch at this point.", this.maxSize); | ||||||
|  |             asyncQuizFetchBuffer.finish(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private Collection<CourseData> getCoursesPage( | ||||||
|  |             final MoodleAPIRestTemplate restTemplate, | ||||||
|  |             final DateTime quizFromTime, | ||||||
|  |             final int page, | ||||||
|  |             final int size) throws JsonParseException, JsonMappingException, IOException { | ||||||
|  | 
 | ||||||
|  |         final String lmsName = getLmsSetupName(); | ||||||
|  |         try { | ||||||
|  |             // get course ids per page | ||||||
|  |             final String fromDate = String.valueOf(Utils.toUnixTimeInSeconds(quizFromTime)); | ||||||
|  |             final String fromElement = String.valueOf(page * size); | ||||||
|  |             final LinkedMultiValueMap<String, String> attributes = new LinkedMultiValueMap<>(); | ||||||
|  |             attributes.add(CRITERIA_FROM_DATE, fromDate); | ||||||
|  |             attributes.add(CRITERIA_LIMIT_FROM, fromElement); | ||||||
|  | 
 | ||||||
|  |             final String courseKeyPageJSON = this.protectedMoodlePageCall | ||||||
|  |                     .protectedRun(() -> restTemplate.callMoodleAPIFunction( | ||||||
|  |                             COURSES_API_FUNCTION_NAME, | ||||||
|  |                             attributes)) | ||||||
|  |                     .getOrThrow(); | ||||||
|  | 
 | ||||||
|  |             final Courses coursePage = this.jsonMapper.readValue(courseKeyPageJSON, Courses.class); | ||||||
|  | 
 | ||||||
|  |             if (coursePage == null) { | ||||||
|  |                 log.error("No CoursePage Response"); | ||||||
|  |                 return Collections.emptyList(); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (coursePage.warnings != null && !coursePage.warnings.isEmpty()) { | ||||||
|  |                 MoodleUtils.logMoodleWarning(coursePage.warnings, lmsName, COURSES_API_FUNCTION_NAME); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             Collection<CourseData> result; | ||||||
|  |             if (coursePage.courses == null || coursePage.courses.isEmpty()) { | ||||||
|  |                 if (log.isDebugEnabled()) { | ||||||
|  |                     log.debug("LMS Setup: {} No courses found on page: {}", lmsName, page); | ||||||
|  |                     if (log.isTraceEnabled()) { | ||||||
|  |                         log.trace("Moodle response: {}", courseKeyPageJSON); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 result = Collections.emptyList(); | ||||||
|  |             } else { | ||||||
|  |                 result = coursePage.courses; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (log.isDebugEnabled()) { | ||||||
|  |                 log.debug("course page with {} courses", result.size()); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             return result; | ||||||
|  |         } catch (final Exception e) { | ||||||
|  |             log.error("LMS Setup: {} Unexpected error while trying to get courses page: ", lmsName, e); | ||||||
|  |             return Collections.emptyList(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private List<QuizData> getQuizzesForIds( | ||||||
|  |             final MoodleAPIRestTemplate restTemplate, | ||||||
|  |             final Set<String> quizIds) { | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|  | 
 | ||||||
|  |             final LmsSetup lmsSetup = this.restTemplateFactory.getApiTemplateDataSupplier().getLmsSetup(); | ||||||
|  | 
 | ||||||
|  |             if (log.isDebugEnabled()) { | ||||||
|  |                 log.debug("Get quizzes for ids: {} and LMSSetup: {}", quizIds, lmsSetup); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // get involved courses and map by course id | ||||||
|  |             final Map<String, CourseData> courseData = getCoursesForIds( | ||||||
|  |                     restTemplate, | ||||||
|  |                     quizIds.stream() | ||||||
|  |                             .map(MoodleUtils::getCourseId) | ||||||
|  |                             .collect(Collectors.toSet())) | ||||||
|  |                                     .stream() | ||||||
|  |                                     .collect(Collectors.toMap(cd -> cd.id, Function.identity())); | ||||||
|  | 
 | ||||||
|  |             // then get all quizzes of courses and filter | ||||||
|  |             final LinkedMultiValueMap<String, String> attributes = new LinkedMultiValueMap<>(); | ||||||
|  |             attributes.put(CRITERIA_COURSE_IDS, new ArrayList<>(courseData.keySet())); | ||||||
|  | 
 | ||||||
|  |             final String quizzesJSON = restTemplate.callMoodleAPIFunction( | ||||||
|  |                     QUIZZES_BY_COURSES_API_FUNCTION_NAME, | ||||||
|  |                     attributes); | ||||||
|  | 
 | ||||||
|  |             final CourseQuizData courseQuizData = this.jsonMapper.readValue( | ||||||
|  |                     quizzesJSON, | ||||||
|  |                     CourseQuizData.class); | ||||||
|  | 
 | ||||||
|  |             if (courseQuizData.warnings != null && !courseQuizData.warnings.isEmpty()) { | ||||||
|  |                 MoodleUtils.logMoodleWarning( | ||||||
|  |                         courseQuizData.warnings, | ||||||
|  |                         lmsSetup.name, | ||||||
|  |                         QUIZZES_BY_COURSES_API_FUNCTION_NAME); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             final Map<String, CourseData> finalCourseDataRef = courseData; | ||||||
|  |             courseQuizData.quizzes | ||||||
|  |                     .stream() | ||||||
|  |                     .forEach(quiz -> MoodleUtils.fillSelectedQuizzes(quizIds, finalCourseDataRef, quiz)); | ||||||
|  | 
 | ||||||
|  |             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() | ||||||
|  |                     .filter(c -> !c.quizzes.isEmpty()) | ||||||
|  |                     .flatMap(cd -> MoodleUtils.quizDataOf( | ||||||
|  |                             lmsSetup, | ||||||
|  |                             cd, | ||||||
|  |                             urlPrefix, | ||||||
|  |                             this.prependShortCourseName).stream()) | ||||||
|  |                     .collect(Collectors.toList()); | ||||||
|  | 
 | ||||||
|  |         } catch (final Exception e) { | ||||||
|  |             log.error("Unexpected error while trying to get quizzes for ids", e); | ||||||
|  |             return Collections.emptyList(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private Collection<CourseData> getCoursesForIds( | ||||||
|  |             final MoodleAPIRestTemplate restTemplate, | ||||||
|  |             final Set<String> courseIds) { | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|  | 
 | ||||||
|  |             final LmsSetup lmsSetup = this.restTemplateFactory.getApiTemplateDataSupplier().getLmsSetup(); | ||||||
|  | 
 | ||||||
|  |             if (log.isDebugEnabled()) { | ||||||
|  |                 log.debug("Get courses for ids: {} on LMS: {}", courseIds, lmsSetup); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             final String joinedIds = StringUtils.join(courseIds, Constants.COMMA); | ||||||
|  | 
 | ||||||
|  |             final LinkedMultiValueMap<String, String> attributes = new LinkedMultiValueMap<>(); | ||||||
|  |             attributes.add(CRITERIA_COURSE_IDS, joinedIds); | ||||||
|  |             final String coursePageJSON = restTemplate.callMoodleAPIFunction( | ||||||
|  |                     COURSES_API_FUNCTION_NAME, | ||||||
|  |                     attributes); | ||||||
|  | 
 | ||||||
|  |             final Courses courses = this.jsonMapper.readValue( | ||||||
|  |                     coursePageJSON, | ||||||
|  |                     Courses.class); | ||||||
|  | 
 | ||||||
|  |             if (courses.courses == null || courses.courses.isEmpty()) { | ||||||
|  |                 log.warn("No courses found for ids: {} on LMS: {}", courseIds, lmsSetup.name); | ||||||
|  | 
 | ||||||
|  |                 if (courses != null && courses.warnings != null && !courses.warnings.isEmpty()) { | ||||||
|  |                     MoodleUtils.logMoodleWarning(courses.warnings, lmsSetup.name, COURSES_API_FUNCTION_NAME); | ||||||
|  |                 } | ||||||
|  |                 return Collections.emptyList(); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             return courses.courses; | ||||||
|  |         } catch (final Exception e) { | ||||||
|  |             log.error("Unexpected error while trying to get courses for ids", e); | ||||||
|  |             return Collections.emptyList(); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private Result<MoodleAPIRestTemplate> getRestTemplate() { |     private Result<MoodleAPIRestTemplate> getRestTemplate() { | ||||||
|         if (this.restTemplate == null) { |         if (this.restTemplate == null) { | ||||||
|             final Result<MoodleAPIRestTemplate> templateRequest = this.moodleRestTemplateFactory |             final Result<MoodleAPIRestTemplate> templateRequest = this.restTemplateFactory | ||||||
|                     .createRestTemplate(); |                     .createRestTemplate(); | ||||||
|             if (templateRequest.hasError()) { |             if (templateRequest.hasError()) { | ||||||
|                 return templateRequest; |                 return templateRequest; | ||||||
|  | @ -156,99 +558,4 @@ public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess impleme | ||||||
|         return Result.of(this.restTemplate); |         return Result.of(this.restTemplate); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // ---- Mapping Classes --- |  | ||||||
| 
 |  | ||||||
|     @JsonIgnoreProperties(ignoreUnknown = true) |  | ||||||
|     private static final class Courses { |  | ||||||
|         final Collection<CourseData> courses; |  | ||||||
|         final Collection<Warning> warnings; |  | ||||||
| 
 |  | ||||||
|         @JsonCreator |  | ||||||
|         protected Courses( |  | ||||||
|                 @JsonProperty(value = "courses") final Collection<CourseData> courses, |  | ||||||
|                 @JsonProperty(value = "warnings") final Collection<Warning> warnings) { |  | ||||||
|             this.courses = courses; |  | ||||||
|             this.warnings = warnings; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** Maps the Moodle course API course data */ |  | ||||||
|     @JsonIgnoreProperties(ignoreUnknown = true) |  | ||||||
|     private static final class CourseData { |  | ||||||
|         final String id; |  | ||||||
|         final String short_name; |  | ||||||
|         final String idnumber; |  | ||||||
|         final String full_name; |  | ||||||
|         final String display_name; |  | ||||||
|         final Long start_date; // unix-time seconds UTC |  | ||||||
|         final Long end_date; // unix-time seconds UTC |  | ||||||
|         final Long time_created; // unix-time seconds UTC |  | ||||||
|         final Collection<CourseQuiz> quizzes = new ArrayList<>(); |  | ||||||
| 
 |  | ||||||
|         @JsonCreator |  | ||||||
|         protected CourseData( |  | ||||||
|                 @JsonProperty(value = "id") final String id, |  | ||||||
|                 @JsonProperty(value = "shortname") final String short_name, |  | ||||||
|                 @JsonProperty(value = "idnumber") final String idnumber, |  | ||||||
|                 @JsonProperty(value = "fullname") final String full_name, |  | ||||||
|                 @JsonProperty(value = "displayname") final String display_name, |  | ||||||
|                 @JsonProperty(value = "startdate") final Long start_date, |  | ||||||
|                 @JsonProperty(value = "enddate") final Long end_date, |  | ||||||
|                 @JsonProperty(value = "timecreated") final Long time_created) { |  | ||||||
| 
 |  | ||||||
|             this.id = id; |  | ||||||
|             this.short_name = short_name; |  | ||||||
|             this.idnumber = idnumber; |  | ||||||
|             this.full_name = full_name; |  | ||||||
|             this.display_name = display_name; |  | ||||||
|             this.start_date = start_date; |  | ||||||
|             this.end_date = end_date; |  | ||||||
|             this.time_created = time_created; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @JsonIgnoreProperties(ignoreUnknown = true) |  | ||||||
|     private static final class CourseQuizData { |  | ||||||
|         final Collection<CourseQuiz> quizzes; |  | ||||||
|         final Collection<Warning> warnings; |  | ||||||
| 
 |  | ||||||
|         @JsonCreator |  | ||||||
|         protected CourseQuizData( |  | ||||||
|                 @JsonProperty(value = "quizzes") final Collection<CourseQuiz> quizzes, |  | ||||||
|                 @JsonProperty(value = "warnings") final Collection<Warning> warnings) { |  | ||||||
|             this.quizzes = quizzes; |  | ||||||
|             this.warnings = warnings; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @JsonIgnoreProperties(ignoreUnknown = true) |  | ||||||
|     static final class CourseQuiz { |  | ||||||
|         final String id; |  | ||||||
|         final String course; |  | ||||||
|         final String course_module; |  | ||||||
|         final String name; |  | ||||||
|         final String intro; // HTML |  | ||||||
|         final Long time_open; // unix-time seconds UTC |  | ||||||
|         final Long time_close; // unix-time seconds UTC |  | ||||||
| 
 |  | ||||||
|         @JsonCreator |  | ||||||
|         protected CourseQuiz( |  | ||||||
|                 @JsonProperty(value = "id") final String id, |  | ||||||
|                 @JsonProperty(value = "course") final String course, |  | ||||||
|                 @JsonProperty(value = "coursemodule") final String course_module, |  | ||||||
|                 @JsonProperty(value = "name") final String name, |  | ||||||
|                 @JsonProperty(value = "intro") final String intro, |  | ||||||
|                 @JsonProperty(value = "timeopen") final Long time_open, |  | ||||||
|                 @JsonProperty(value = "timeclose") final Long time_close) { |  | ||||||
| 
 |  | ||||||
|             this.id = id; |  | ||||||
|             this.course = course; |  | ||||||
|             this.course_module = course_module; |  | ||||||
|             this.name = name; |  | ||||||
|             this.intro = intro; |  | ||||||
|             this.time_open = time_open; |  | ||||||
|             this.time_close = time_close; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -8,25 +8,116 @@ | ||||||
| 
 | 
 | ||||||
| package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.plugin; | package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.plugin; | ||||||
| 
 | 
 | ||||||
|  | import java.util.ArrayList; | ||||||
|  | import java.util.Arrays; | ||||||
|  | import java.util.HashMap; | ||||||
|  | import java.util.List; | ||||||
|  | import java.util.Map; | ||||||
|  | 
 | ||||||
|  | import org.apache.commons.lang3.StringUtils; | ||||||
|  | import org.slf4j.Logger; | ||||||
|  | import org.slf4j.LoggerFactory; | ||||||
|  | import org.springframework.util.LinkedMultiValueMap; | ||||||
|  | 
 | ||||||
|  | import ch.ethz.seb.sebserver.gbl.Constants; | ||||||
|  | import ch.ethz.seb.sebserver.gbl.api.JSONMapper; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.exam.Exam; | import ch.ethz.seb.sebserver.gbl.model.exam.Exam; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction; | import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType; | import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult; | import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult; | ||||||
| import ch.ethz.seb.sebserver.gbl.util.Result; | import ch.ethz.seb.sebserver.gbl.util.Result; | ||||||
|  | import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamConfigurationValueService; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionAPI; | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionAPI; | ||||||
|  | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionService; | ||||||
|  | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleAPIRestTemplate; | ||||||
|  | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory; | ||||||
|  | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils; | ||||||
|  | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils.MoodleQuizRestriction; | ||||||
| 
 | 
 | ||||||
| public class MoodlePluginCourseRestriction implements SEBRestrictionAPI { | public class MoodlePluginCourseRestriction implements SEBRestrictionAPI { | ||||||
| 
 | 
 | ||||||
|  |     private static final Logger log = LoggerFactory.getLogger(MoodlePluginCourseRestriction.class); | ||||||
|  | 
 | ||||||
|  |     public static final String RESTRICTION_GET_FUNCTION_NAME = "quizaccess_sebserver_get_restriction"; | ||||||
|  |     public static final String RESTRICTION_SET_FUNCTION_NAME = "quizaccess_sebserver_set_restriction"; | ||||||
|  | 
 | ||||||
|  |     public static final String ATTRIBUTE_QUIZ_ID = "quiz_id"; | ||||||
|  |     public static final String ATTRIBUTE_CONFIG_KEYS = "config_keys"; | ||||||
|  |     public static final String ATTRIBUTE_BROWSER_EXAM_KEYS = "browser_exam_keys"; | ||||||
|  |     public static final String ATTRIBUTE_QUIT_URL = "quit_link"; | ||||||
|  |     public static final String ATTRIBUTE_QUIT_SECRET = "quit_secret"; | ||||||
|  | 
 | ||||||
|  |     private final JSONMapper jsonMapper; | ||||||
|  |     private final MoodleRestTemplateFactory restTemplateFactory; | ||||||
|  |     private final ExamConfigurationValueService examConfigurationValueService; | ||||||
|  | 
 | ||||||
|  |     private MoodleAPIRestTemplate restTemplate; | ||||||
|  | 
 | ||||||
|  |     public MoodlePluginCourseRestriction( | ||||||
|  |             final JSONMapper jsonMapper, | ||||||
|  |             final MoodleRestTemplateFactory restTemplateFactory, | ||||||
|  |             final ExamConfigurationValueService examConfigurationValueService) { | ||||||
|  | 
 | ||||||
|  |         this.jsonMapper = jsonMapper; | ||||||
|  |         this.restTemplateFactory = restTemplateFactory; | ||||||
|  |         this.examConfigurationValueService = examConfigurationValueService; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     @Override |     @Override | ||||||
|     public LmsSetupTestResult testCourseRestrictionAPI() { |     public LmsSetupTestResult testCourseRestrictionAPI() { | ||||||
|         // TODO Auto-generated method stub | 
 | ||||||
|  |         final LmsSetupTestResult attributesCheck = this.restTemplateFactory.test(); | ||||||
|  |         if (!attributesCheck.isOk()) { | ||||||
|  |             return attributesCheck; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         final Result<MoodleAPIRestTemplate> restTemplateRequest = getRestTemplate(); | ||||||
|  |         if (restTemplateRequest.hasError()) { | ||||||
|  |             final String message = "Failed to gain access token from Moodle Rest API:\n tried token endpoints: " + | ||||||
|  |                     this.restTemplateFactory.getKnownTokenAccessPaths(); | ||||||
|  |             log.error(message + " cause: {}", restTemplateRequest.getError().getMessage()); | ||||||
|  |             return LmsSetupTestResult.ofTokenRequestError(LmsType.MOODLE_PLUGIN, message); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|  |             final MoodleAPIRestTemplate restTemplate = restTemplateRequest.get(); | ||||||
|  |             restTemplate.testAPIConnection( | ||||||
|  |                     RESTRICTION_GET_FUNCTION_NAME, | ||||||
|  |                     RESTRICTION_SET_FUNCTION_NAME); | ||||||
|  | 
 | ||||||
|  |         } catch (final RuntimeException e) { | ||||||
|  |             log.error("Failed to access Moodle course API: ", e); | ||||||
|  |             return LmsSetupTestResult.ofQuizAccessAPIError(LmsType.MOODLE_PLUGIN, e.getMessage()); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         return LmsSetupTestResult.ofOkay(LmsType.MOODLE_PLUGIN); |         return LmsSetupTestResult.ofOkay(LmsType.MOODLE_PLUGIN); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public Result<SEBRestriction> getSEBClientRestriction(final Exam exam) { |     public Result<SEBRestriction> getSEBClientRestriction(final Exam exam) { | ||||||
|         // TODO Auto-generated method stub |         return getRestTemplate().map(restTemplate -> { | ||||||
|         return null; | 
 | ||||||
|  |             if (log.isDebugEnabled()) { | ||||||
|  |                 log.debug("Get SEB Client restriction on exam: {}", exam); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             final String quizId = MoodleUtils.getQuizId(exam.getExternalId()); | ||||||
|  |             final LinkedMultiValueMap<String, String> addQuery = new LinkedMultiValueMap<>(); | ||||||
|  |             addQuery.add(ATTRIBUTE_QUIZ_ID, quizId); | ||||||
|  | 
 | ||||||
|  |             final String srJSON = restTemplate.callMoodleAPIFunction(RESTRICTION_GET_FUNCTION_NAME, addQuery); | ||||||
|  | 
 | ||||||
|  |             try { | ||||||
|  | 
 | ||||||
|  |                 final MoodleQuizRestriction moodleRestriction = this.jsonMapper.readValue( | ||||||
|  |                         srJSON, | ||||||
|  |                         MoodleUtils.MoodleQuizRestriction.class); | ||||||
|  | 
 | ||||||
|  |                 return toSEBRestriction(exam, moodleRestriction); | ||||||
|  |             } catch (final Exception e) { | ||||||
|  |                 throw new RuntimeException("Unexpected error while get SEB restriction: ", e); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|  | @ -34,14 +125,113 @@ public class MoodlePluginCourseRestriction implements SEBRestrictionAPI { | ||||||
|             final Exam exam, |             final Exam exam, | ||||||
|             final SEBRestriction sebRestrictionData) { |             final SEBRestriction sebRestrictionData) { | ||||||
| 
 | 
 | ||||||
|         // TODO Auto-generated method stub |         return Result.tryCatch(() -> { | ||||||
|         return null; | 
 | ||||||
|  |             if (log.isDebugEnabled()) { | ||||||
|  |                 log.debug("Apply SEB Client restriction on exam: {}", exam); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             final String quizId = MoodleUtils.getQuizId(exam.getExternalId()); | ||||||
|  |             final LinkedMultiValueMap<String, String> addQuery = new LinkedMultiValueMap<>(); | ||||||
|  |             addQuery.add(ATTRIBUTE_QUIZ_ID, quizId); | ||||||
|  | 
 | ||||||
|  |             final ArrayList<String> beks = new ArrayList<>(sebRestrictionData.browserExamKeys); | ||||||
|  |             final ArrayList<String> configKeys = new ArrayList<>(sebRestrictionData.configKeys); | ||||||
|  |             final String quitLink = this.examConfigurationValueService.getQuitLink(exam.id); | ||||||
|  |             final String quitSecret = this.examConfigurationValueService.getQuitSecret(exam.id); | ||||||
|  |             final String additionalBEK = sebRestrictionData.additionalProperties.get( | ||||||
|  |                     SEBRestrictionService.ADDITIONAL_ATTR_ALTERNATIVE_SEB_BEK); | ||||||
|  |             if (additionalBEK != null) { | ||||||
|  |                 beks.add(additionalBEK); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             final LinkedMultiValueMap<String, String> queryAttributes = new LinkedMultiValueMap<>(); | ||||||
|  |             queryAttributes.put(ATTRIBUTE_CONFIG_KEYS, configKeys); | ||||||
|  |             queryAttributes.put(ATTRIBUTE_BROWSER_EXAM_KEYS, beks); | ||||||
|  |             queryAttributes.add(ATTRIBUTE_QUIT_URL, quitLink); | ||||||
|  |             queryAttributes.add(ATTRIBUTE_QUIT_SECRET, quitSecret); | ||||||
|  | 
 | ||||||
|  |             final String srJSON = this.restTemplate.callMoodleAPIFunction( | ||||||
|  |                     RESTRICTION_SET_FUNCTION_NAME, | ||||||
|  |                     addQuery, | ||||||
|  |                     queryAttributes); | ||||||
|  | 
 | ||||||
|  |             try { | ||||||
|  | 
 | ||||||
|  |                 final MoodleQuizRestriction moodleRestriction = this.jsonMapper.readValue( | ||||||
|  |                         srJSON, | ||||||
|  |                         MoodleUtils.MoodleQuizRestriction.class); | ||||||
|  | 
 | ||||||
|  |                 return toSEBRestriction(exam, moodleRestriction); | ||||||
|  |             } catch (final Exception e) { | ||||||
|  |                 throw new RuntimeException("Unexpected error while get SEB restriction: ", e); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public Result<Exam> releaseSEBClientRestriction(final Exam exam) { |     public Result<Exam> releaseSEBClientRestriction(final Exam exam) { | ||||||
|         // TODO Auto-generated method stub |         return Result.tryCatch(() -> { | ||||||
|         return null; |             if (log.isDebugEnabled()) { | ||||||
|  |                 log.debug("Release SEB Client restriction on exam: {}", exam); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             final String quizId = MoodleUtils.getQuizId(exam.getExternalId()); | ||||||
|  |             final LinkedMultiValueMap<String, String> addQuery = new LinkedMultiValueMap<>(); | ||||||
|  |             addQuery.add(ATTRIBUTE_QUIZ_ID, quizId); | ||||||
|  | 
 | ||||||
|  |             final String quitLink = this.examConfigurationValueService.getQuitLink(exam.id); | ||||||
|  |             final String quitSecret = this.examConfigurationValueService.getQuitSecret(exam.id); | ||||||
|  | 
 | ||||||
|  |             final LinkedMultiValueMap<String, String> queryAttributes = new LinkedMultiValueMap<>(); | ||||||
|  |             queryAttributes.add(ATTRIBUTE_QUIT_URL, quitLink); | ||||||
|  |             queryAttributes.add(ATTRIBUTE_QUIT_SECRET, quitSecret); | ||||||
|  | 
 | ||||||
|  |             this.restTemplate.callMoodleAPIFunction( | ||||||
|  |                     RESTRICTION_SET_FUNCTION_NAME, | ||||||
|  |                     addQuery, | ||||||
|  |                     queryAttributes); | ||||||
|  | 
 | ||||||
|  |             return exam; | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private SEBRestriction toSEBRestriction(final Exam exam, final MoodleQuizRestriction moodleRestriction) { | ||||||
|  |         final List<String> configKeys = Arrays.asList(StringUtils.split( | ||||||
|  |                 moodleRestriction.config_keys, | ||||||
|  |                 Constants.LIST_SEPARATOR)); | ||||||
|  |         final List<String> browserExamKeys = Arrays.asList(StringUtils.split( | ||||||
|  |                 moodleRestriction.browser_exam_keys, | ||||||
|  |                 Constants.LIST_SEPARATOR)); | ||||||
|  |         final Map<String, String> additionalProperties = new HashMap<>(); | ||||||
|  |         additionalProperties.put(ATTRIBUTE_QUIT_URL, moodleRestriction.quit_link); | ||||||
|  | 
 | ||||||
|  |         final String additionalBEK = exam.getAdditionalAttribute( | ||||||
|  |                 SEBRestrictionService.ADDITIONAL_ATTR_ALTERNATIVE_SEB_BEK); | ||||||
|  | 
 | ||||||
|  |         if (additionalBEK != null) { | ||||||
|  |             browserExamKeys.remove(additionalBEK); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return new SEBRestriction( | ||||||
|  |                 exam.id, | ||||||
|  |                 configKeys, | ||||||
|  |                 browserExamKeys, | ||||||
|  |                 additionalProperties); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private Result<MoodleAPIRestTemplate> getRestTemplate() { | ||||||
|  |         if (this.restTemplate == null) { | ||||||
|  |             final Result<MoodleAPIRestTemplate> templateRequest = this.restTemplateFactory | ||||||
|  |                     .createRestTemplate(); | ||||||
|  |             if (templateRequest.hasError()) { | ||||||
|  |                 return templateRequest; | ||||||
|  |             } else { | ||||||
|  |                 this.restTemplate = templateRequest.get(); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return Result.of(this.restTemplate); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -24,10 +24,13 @@ import ch.ethz.seb.sebserver.gbl.client.ClientCredentialService; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType; | import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType; | ||||||
| import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; | import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; | ||||||
| import ch.ethz.seb.sebserver.gbl.util.Result; | import ch.ethz.seb.sebserver.gbl.util.Result; | ||||||
|  | import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamConfigurationValueService; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier; | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate; | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplateFactory; | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplateFactory; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.LmsAPITemplateAdapter; | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.LmsAPITemplateAdapter; | ||||||
|  | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MockupRestTemplateFactory; | ||||||
|  | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodlePluginCheck; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory; | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory; | ||||||
| 
 | 
 | ||||||
| @Lazy | @Lazy | ||||||
|  | @ -41,6 +44,7 @@ public class MooldePluginLmsAPITemplateFactory implements LmsAPITemplateFactory | ||||||
|     private final AsyncService asyncService; |     private final AsyncService asyncService; | ||||||
|     private final Environment environment; |     private final Environment environment; | ||||||
|     private final ClientCredentialService clientCredentialService; |     private final ClientCredentialService clientCredentialService; | ||||||
|  |     private final ExamConfigurationValueService examConfigurationValueService; | ||||||
|     private final ClientHttpRequestFactoryService clientHttpRequestFactoryService; |     private final ClientHttpRequestFactoryService clientHttpRequestFactoryService; | ||||||
|     private final ApplicationContext applicationContext; |     private final ApplicationContext applicationContext; | ||||||
|     private final String[] alternativeTokenRequestPaths; |     private final String[] alternativeTokenRequestPaths; | ||||||
|  | @ -52,6 +56,7 @@ public class MooldePluginLmsAPITemplateFactory implements LmsAPITemplateFactory | ||||||
|             final AsyncService asyncService, |             final AsyncService asyncService, | ||||||
|             final Environment environment, |             final Environment environment, | ||||||
|             final ClientCredentialService clientCredentialService, |             final ClientCredentialService clientCredentialService, | ||||||
|  |             final ExamConfigurationValueService examConfigurationValueService, | ||||||
|             final ClientHttpRequestFactoryService clientHttpRequestFactoryService, |             final ClientHttpRequestFactoryService clientHttpRequestFactoryService, | ||||||
|             final ApplicationContext applicationContext, |             final ApplicationContext applicationContext, | ||||||
|             @Value("${sebserver.webservice.lms.moodle.api.token.request.paths:}") final String alternativeTokenRequestPaths) { |             @Value("${sebserver.webservice.lms.moodle.api.token.request.paths:}") final String alternativeTokenRequestPaths) { | ||||||
|  | @ -62,6 +67,7 @@ public class MooldePluginLmsAPITemplateFactory implements LmsAPITemplateFactory | ||||||
|         this.asyncService = asyncService; |         this.asyncService = asyncService; | ||||||
|         this.environment = environment; |         this.environment = environment; | ||||||
|         this.clientCredentialService = clientCredentialService; |         this.clientCredentialService = clientCredentialService; | ||||||
|  |         this.examConfigurationValueService = examConfigurationValueService; | ||||||
|         this.clientHttpRequestFactoryService = clientHttpRequestFactoryService; |         this.clientHttpRequestFactoryService = clientHttpRequestFactoryService; | ||||||
|         this.applicationContext = applicationContext; |         this.applicationContext = applicationContext; | ||||||
|         this.alternativeTokenRequestPaths = (alternativeTokenRequestPaths != null) |         this.alternativeTokenRequestPaths = (alternativeTokenRequestPaths != null) | ||||||
|  | @ -78,19 +84,27 @@ public class MooldePluginLmsAPITemplateFactory implements LmsAPITemplateFactory | ||||||
|     public Result<LmsAPITemplate> create(final APITemplateDataSupplier apiTemplateDataSupplier) { |     public Result<LmsAPITemplate> create(final APITemplateDataSupplier apiTemplateDataSupplier) { | ||||||
|         return Result.tryCatch(() -> { |         return Result.tryCatch(() -> { | ||||||
| 
 | 
 | ||||||
|             final MoodleRestTemplateFactory moodleRestTemplateFactory = new MoodleRestTemplateFactory( | //            final MoodleRestTemplateFactory moodleRestTemplateFactory = new MoodleRestTemplateFactoryImpl( | ||||||
|                     this.jsonMapper, | //                    this.jsonMapper, | ||||||
|                     apiTemplateDataSupplier, | //                    apiTemplateDataSupplier, | ||||||
|                     this.clientCredentialService, | //                    this.clientCredentialService, | ||||||
|                     this.clientHttpRequestFactoryService, | //                    this.clientHttpRequestFactoryService, | ||||||
|                     this.alternativeTokenRequestPaths); | //                    this.alternativeTokenRequestPaths); | ||||||
|  | 
 | ||||||
|  |             final MoodleRestTemplateFactory moodleRestTemplateFactory = | ||||||
|  |                     new MockupRestTemplateFactory(apiTemplateDataSupplier); | ||||||
| 
 | 
 | ||||||
|             final MoodlePluginCourseAccess moodlePluginCourseAccess = new MoodlePluginCourseAccess( |             final MoodlePluginCourseAccess moodlePluginCourseAccess = new MoodlePluginCourseAccess( | ||||||
|                     this.jsonMapper, |                     this.jsonMapper, | ||||||
|  |                     this.asyncService, | ||||||
|                     moodleRestTemplateFactory, |                     moodleRestTemplateFactory, | ||||||
|                     this.cacheManager); |                     this.cacheManager, | ||||||
|  |                     this.environment); | ||||||
| 
 | 
 | ||||||
|             final MoodlePluginCourseRestriction moodlePluginCourseRestriction = new MoodlePluginCourseRestriction(); |             final MoodlePluginCourseRestriction moodlePluginCourseRestriction = new MoodlePluginCourseRestriction( | ||||||
|  |                     this.jsonMapper, | ||||||
|  |                     moodleRestTemplateFactory, | ||||||
|  |                     this.examConfigurationValueService); | ||||||
| 
 | 
 | ||||||
|             return new LmsAPITemplateAdapter( |             return new LmsAPITemplateAdapter( | ||||||
|                     this.asyncService, |                     this.asyncService, | ||||||
|  |  | ||||||
|  | @ -47,7 +47,6 @@ import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType; | import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult; | import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails; | import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails; | ||||||
| import ch.ethz.seb.sebserver.gbl.util.Cryptor; |  | ||||||
| import ch.ethz.seb.sebserver.gbl.util.Result; | import ch.ethz.seb.sebserver.gbl.util.Result; | ||||||
| import ch.ethz.seb.sebserver.gbl.util.Utils; | import ch.ethz.seb.sebserver.gbl.util.Utils; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; | import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; | ||||||
|  | @ -68,14 +67,10 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm | ||||||
|     private static final String ADDITIONAL_ATTR_QUIT_LINK = "ADDITIONAL_ATTR_QUIT_LINK"; |     private static final String ADDITIONAL_ATTR_QUIT_LINK = "ADDITIONAL_ATTR_QUIT_LINK"; | ||||||
|     private static final String ADDITIONAL_ATTR_QUIT_SECRET = "ADDITIONAL_ATTR_QUIT_SECRET"; |     private static final String ADDITIONAL_ATTR_QUIT_SECRET = "ADDITIONAL_ATTR_QUIT_SECRET"; | ||||||
| 
 | 
 | ||||||
|     private static final String CONFIG_ATTR_NAME_QUIT_LINK = "quitURL"; |  | ||||||
|     private static final String CONFIG_ATTR_NAME_QUIT_SECRET = "hashedQuitPassword"; |  | ||||||
| 
 |  | ||||||
|     private final ClientHttpRequestFactoryService clientHttpRequestFactoryService; |     private final ClientHttpRequestFactoryService clientHttpRequestFactoryService; | ||||||
|     private final ClientCredentialService clientCredentialService; |     private final ClientCredentialService clientCredentialService; | ||||||
|     private final APITemplateDataSupplier apiTemplateDataSupplier; |     private final APITemplateDataSupplier apiTemplateDataSupplier; | ||||||
|     private final ExamConfigurationValueService examConfigurationValueService; |     private final ExamConfigurationValueService examConfigurationValueService; | ||||||
|     private final Cryptor cryptor; |  | ||||||
|     private final Long lmsSetupId; |     private final Long lmsSetupId; | ||||||
| 
 | 
 | ||||||
|     private OlatLmsRestTemplate cachedRestTemplate; |     private OlatLmsRestTemplate cachedRestTemplate; | ||||||
|  | @ -85,7 +80,6 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm | ||||||
|             final ClientCredentialService clientCredentialService, |             final ClientCredentialService clientCredentialService, | ||||||
|             final APITemplateDataSupplier apiTemplateDataSupplier, |             final APITemplateDataSupplier apiTemplateDataSupplier, | ||||||
|             final ExamConfigurationValueService examConfigurationValueService, |             final ExamConfigurationValueService examConfigurationValueService, | ||||||
|             final Cryptor cryptor, |  | ||||||
|             final CacheManager cacheManager) { |             final CacheManager cacheManager) { | ||||||
| 
 | 
 | ||||||
|         super(cacheManager); |         super(cacheManager); | ||||||
|  | @ -94,7 +88,6 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm | ||||||
|         this.clientCredentialService = clientCredentialService; |         this.clientCredentialService = clientCredentialService; | ||||||
|         this.apiTemplateDataSupplier = apiTemplateDataSupplier; |         this.apiTemplateDataSupplier = apiTemplateDataSupplier; | ||||||
|         this.examConfigurationValueService = examConfigurationValueService; |         this.examConfigurationValueService = examConfigurationValueService; | ||||||
|         this.cryptor = cryptor; |  | ||||||
|         this.lmsSetupId = apiTemplateDataSupplier.getLmsSetup().id; |         this.lmsSetupId = apiTemplateDataSupplier.getLmsSetup().id; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -357,8 +350,8 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm | ||||||
|         final RestrictionDataPost post = new RestrictionDataPost(); |         final RestrictionDataPost post = new RestrictionDataPost(); | ||||||
|         post.browserExamKeys = new ArrayList<>(restriction.browserExamKeys); |         post.browserExamKeys = new ArrayList<>(restriction.browserExamKeys); | ||||||
|         post.configKeys = new ArrayList<>(restriction.configKeys); |         post.configKeys = new ArrayList<>(restriction.configKeys); | ||||||
|         post.quitLink = this.getQuitLink(restriction.examId); |         post.quitLink = this.examConfigurationValueService.getQuitLink(restriction.examId); | ||||||
|         post.quitSecret = this.getQuitSecret(restriction.examId); |         post.quitSecret = this.examConfigurationValueService.getQuitSecret(restriction.examId); | ||||||
|         final RestrictionData r = |         final RestrictionData r = | ||||||
|                 this.apiPost(restTemplate, url, post, RestrictionDataPost.class, RestrictionData.class); |                 this.apiPost(restTemplate, url, post, RestrictionDataPost.class, RestrictionData.class); | ||||||
|         return new SEBRestriction(Long.valueOf(id), r.configKeys, r.browserExamKeys, new HashMap<String, String>()); |         return new SEBRestriction(Long.valueOf(id), r.configKeys, r.browserExamKeys, new HashMap<String, String>()); | ||||||
|  | @ -476,43 +469,4 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private String getQuitSecret(final Long examId) { |  | ||||||
|         try { |  | ||||||
| 
 |  | ||||||
|             final String quitSecretEncrypted = this.examConfigurationValueService.getMappedDefaultConfigAttributeValue( |  | ||||||
|                     examId, |  | ||||||
|                     CONFIG_ATTR_NAME_QUIT_SECRET); |  | ||||||
| 
 |  | ||||||
|             if (StringUtils.isNotEmpty(quitSecretEncrypted)) { |  | ||||||
|                 try { |  | ||||||
| 
 |  | ||||||
|                     return this.cryptor |  | ||||||
|                             .decrypt(quitSecretEncrypted) |  | ||||||
|                             .getOrThrow() |  | ||||||
|                             .toString(); |  | ||||||
| 
 |  | ||||||
|                 } catch (final Exception e) { |  | ||||||
|                     log.error("Failed to decrypt quitSecret: ", e); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } catch (final Exception e) { |  | ||||||
|             log.error("Failed to get SEB restriction with quit secret: ", e); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return null; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private String getQuitLink(final Long examId) { |  | ||||||
|         try { |  | ||||||
| 
 |  | ||||||
|             return this.examConfigurationValueService.getMappedDefaultConfigAttributeValue( |  | ||||||
|                     examId, |  | ||||||
|                     CONFIG_ATTR_NAME_QUIT_LINK); |  | ||||||
| 
 |  | ||||||
|         } catch (final Exception e) { |  | ||||||
|             log.error("Failed to get SEB restriction with quit link: ", e); |  | ||||||
|             return null; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -18,7 +18,6 @@ import ch.ethz.seb.sebserver.gbl.async.AsyncService; | ||||||
| import ch.ethz.seb.sebserver.gbl.client.ClientCredentialService; | import ch.ethz.seb.sebserver.gbl.client.ClientCredentialService; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType; | import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType; | ||||||
| import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; | import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; | ||||||
| import ch.ethz.seb.sebserver.gbl.util.Cryptor; |  | ||||||
| import ch.ethz.seb.sebserver.gbl.util.Result; | import ch.ethz.seb.sebserver.gbl.util.Result; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamConfigurationValueService; | import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamConfigurationValueService; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier; | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier; | ||||||
|  | @ -44,7 +43,6 @@ public class OlatLmsAPITemplateFactory implements LmsAPITemplateFactory { | ||||||
|     private final AsyncService asyncService; |     private final AsyncService asyncService; | ||||||
|     private final Environment environment; |     private final Environment environment; | ||||||
|     private final CacheManager cacheManager; |     private final CacheManager cacheManager; | ||||||
|     private final Cryptor cryptor; |  | ||||||
| 
 | 
 | ||||||
|     public OlatLmsAPITemplateFactory( |     public OlatLmsAPITemplateFactory( | ||||||
|             final ClientHttpRequestFactoryService clientHttpRequestFactoryService, |             final ClientHttpRequestFactoryService clientHttpRequestFactoryService, | ||||||
|  | @ -52,8 +50,7 @@ public class OlatLmsAPITemplateFactory implements LmsAPITemplateFactory { | ||||||
|             final ExamConfigurationValueService examConfigurationValueService, |             final ExamConfigurationValueService examConfigurationValueService, | ||||||
|             final AsyncService asyncService, |             final AsyncService asyncService, | ||||||
|             final Environment environment, |             final Environment environment, | ||||||
|             final CacheManager cacheManager, |             final CacheManager cacheManager) { | ||||||
|             final Cryptor cryptor) { |  | ||||||
| 
 | 
 | ||||||
|         this.clientHttpRequestFactoryService = clientHttpRequestFactoryService; |         this.clientHttpRequestFactoryService = clientHttpRequestFactoryService; | ||||||
|         this.clientCredentialService = clientCredentialService; |         this.clientCredentialService = clientCredentialService; | ||||||
|  | @ -61,7 +58,6 @@ public class OlatLmsAPITemplateFactory implements LmsAPITemplateFactory { | ||||||
|         this.asyncService = asyncService; |         this.asyncService = asyncService; | ||||||
|         this.environment = environment; |         this.environment = environment; | ||||||
|         this.cacheManager = cacheManager; |         this.cacheManager = cacheManager; | ||||||
|         this.cryptor = cryptor; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|  | @ -72,13 +68,14 @@ public class OlatLmsAPITemplateFactory implements LmsAPITemplateFactory { | ||||||
|     @Override |     @Override | ||||||
|     public Result<LmsAPITemplate> create(final APITemplateDataSupplier apiTemplateDataSupplier) { |     public Result<LmsAPITemplate> create(final APITemplateDataSupplier apiTemplateDataSupplier) { | ||||||
|         return Result.tryCatch(() -> { |         return Result.tryCatch(() -> { | ||||||
|  | 
 | ||||||
|             final OlatLmsAPITemplate olatLmsAPITemplate = new OlatLmsAPITemplate( |             final OlatLmsAPITemplate olatLmsAPITemplate = new OlatLmsAPITemplate( | ||||||
|                     this.clientHttpRequestFactoryService, |                     this.clientHttpRequestFactoryService, | ||||||
|                     this.clientCredentialService, |                     this.clientCredentialService, | ||||||
|                     apiTemplateDataSupplier, |                     apiTemplateDataSupplier, | ||||||
|                     this.examConfigurationValueService, |                     this.examConfigurationValueService, | ||||||
|                     this.cryptor, |  | ||||||
|                     this.cacheManager); |                     this.cacheManager); | ||||||
|  | 
 | ||||||
|             return new LmsAPITemplateAdapter( |             return new LmsAPITemplateAdapter( | ||||||
|                     this.asyncService, |                     this.asyncService, | ||||||
|                     this.environment, |                     this.environment, | ||||||
|  |  | ||||||
|  | @ -38,7 +38,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService; | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate; | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionService; | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionService; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.legacy.MoodleCourseAccess; | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamFinishedEvent; | import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamFinishedEvent; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamResetEvent; | import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamResetEvent; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamStartedEvent; | import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamStartedEvent; | ||||||
|  | @ -402,7 +402,7 @@ class ExamUpdateHandler { | ||||||
|                     log.debug("Found formerName quiz name: {}", exam.name); |                     log.debug("Found formerName quiz name: {}", exam.name); | ||||||
| 
 | 
 | ||||||
|                     // get the course name identifier |                     // get the course name identifier | ||||||
|                     final String shortname = MoodleCourseAccess.getShortname(quizId); |                     final String shortname = MoodleUtils.getShortname(quizId); | ||||||
|                     if (StringUtils.isNotBlank(shortname)) { |                     if (StringUtils.isNotBlank(shortname)) { | ||||||
| 
 | 
 | ||||||
|                         log.debug("Using short-name: {} for recovering", shortname); |                         log.debug("Using short-name: {} for recovering", shortname); | ||||||
|  | @ -412,7 +412,7 @@ class ExamUpdateHandler { | ||||||
|                                 .getOrThrow() |                                 .getOrThrow() | ||||||
|                                 .stream() |                                 .stream() | ||||||
|                                 .filter(quiz -> { |                                 .filter(quiz -> { | ||||||
|                                     final String qShortName = MoodleCourseAccess.getShortname(quiz.id); |                                     final String qShortName = MoodleUtils.getShortname(quiz.id); | ||||||
|                                     return qShortName != null && qShortName.equals(shortname); |                                     return qShortName != null && qShortName.equals(shortname); | ||||||
|                                 }) |                                 }) | ||||||
|                                 .filter(quiz -> exam.name.equals(quiz.name)) |                                 .filter(quiz -> exam.name.equals(quiz.name)) | ||||||
|  |  | ||||||
|  | @ -51,6 +51,7 @@ import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; | ||||||
| import ch.ethz.seb.sebserver.gbl.util.Utils; | import ch.ethz.seb.sebserver.gbl.util.Utils; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.dao.LmsSetupDAO; | import ch.ethz.seb.sebserver.webservice.servicelayer.dao.LmsSetupDAO; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.dao.SEBClientConfigDAO; | import ch.ethz.seb.sebserver.webservice.servicelayer.dao.SEBClientConfigDAO; | ||||||
|  | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionService; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService; | import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientConnectionService; | import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientConnectionService; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientSessionService; | import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientSessionService; | ||||||
|  | @ -479,7 +480,7 @@ public class ExamAPI_V1_Controller { | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|         this.examSessionService.getRunningExam(examId) |         this.examSessionService.getRunningExam(examId) | ||||||
|                 .map(exam -> exam.getAdditionalAttribute(Exam.ADDITIONAL_ATTR_ALTERNATIVE_SEB_BEK)) |                 .map(exam -> exam.getAdditionalAttribute(SEBRestrictionService.ADDITIONAL_ATTR_ALTERNATIVE_SEB_BEK)) | ||||||
|                 .onSuccess(bek -> response.setHeader(API.EXAM_API_EXAM_ALT_BEK, bek)); |                 .onSuccess(bek -> response.setHeader(API.EXAM_API_EXAM_ALT_BEK, bek)); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -29,7 +29,6 @@ import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamStatus; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamType; | import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamType; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction; | import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; | import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; | ||||||
| import ch.ethz.seb.sebserver.gbl.util.Cryptor; |  | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamConfigurationValueService; | import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamConfigurationValueService; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier; | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.olat.OlatLmsAPITemplate; | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.olat.OlatLmsAPITemplate; | ||||||
|  | @ -42,8 +41,6 @@ public class OlatLmsAPITemplateTest extends AdministrationAPIIntegrationTester { | ||||||
|     @Autowired |     @Autowired | ||||||
|     private ExamConfigurationValueService examConfigurationValueService; |     private ExamConfigurationValueService examConfigurationValueService; | ||||||
|     @Autowired |     @Autowired | ||||||
|     private Cryptor cryptor; |  | ||||||
|     @Autowired |  | ||||||
|     private CacheManager cacheManager; |     private CacheManager cacheManager; | ||||||
| 
 | 
 | ||||||
|     @Test |     @Test | ||||||
|  | @ -59,7 +56,6 @@ public class OlatLmsAPITemplateTest extends AdministrationAPIIntegrationTester { | ||||||
|                 null, |                 null, | ||||||
|                 apiTemplateDataSupplier, |                 apiTemplateDataSupplier, | ||||||
|                 this.examConfigurationValueService, |                 this.examConfigurationValueService, | ||||||
|                 this.cryptor, |  | ||||||
|                 this.cacheManager); |                 this.cacheManager); | ||||||
| 
 | 
 | ||||||
|         Mockito.when(restTemplateMock.exchange(Mockito.any(), Mockito.any(), Mockito.any(), |         Mockito.when(restTemplateMock.exchange(Mockito.any(), Mockito.any(), Mockito.any(), | ||||||
|  |  | ||||||
|  | @ -29,8 +29,8 @@ import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult.ErrorType; | import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult.ErrorType; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails; | import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails; | ||||||
| import ch.ethz.seb.sebserver.gbl.util.Result; | import ch.ethz.seb.sebserver.gbl.util.Result; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory; | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactoryImpl; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory.MoodleAPIRestTemplateImpl; | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactoryImpl.MoodleAPIRestTemplateImpl; | ||||||
| 
 | 
 | ||||||
| public class MoodleCourseAccessTest { | public class MoodleCourseAccessTest { | ||||||
| 
 | 
 | ||||||
|  | @ -42,7 +42,7 @@ public class MoodleCourseAccessTest { | ||||||
|     @Test |     @Test | ||||||
|     public void testGetExamineeAccountDetails() { |     public void testGetExamineeAccountDetails() { | ||||||
| 
 | 
 | ||||||
|         final MoodleRestTemplateFactory moodleRestTemplateFactory = mock(MoodleRestTemplateFactory.class); |         final MoodleRestTemplateFactoryImpl moodleRestTemplateFactory = mock(MoodleRestTemplateFactoryImpl.class); | ||||||
|         final MoodleAPIRestTemplateImpl moodleAPIRestTemplate = mock(MoodleAPIRestTemplateImpl.class); |         final MoodleAPIRestTemplateImpl moodleAPIRestTemplate = mock(MoodleAPIRestTemplateImpl.class); | ||||||
|         when(moodleRestTemplateFactory.createRestTemplate()).thenReturn(Result.of(moodleAPIRestTemplate)); |         when(moodleRestTemplateFactory.createRestTemplate()).thenReturn(Result.of(moodleAPIRestTemplate)); | ||||||
|         when(moodleAPIRestTemplate.callMoodleAPIFunction( |         when(moodleAPIRestTemplate.callMoodleAPIFunction( | ||||||
|  | @ -118,7 +118,7 @@ public class MoodleCourseAccessTest { | ||||||
| 
 | 
 | ||||||
|     @Test |     @Test | ||||||
|     public void testInitAPIAccessError1() { |     public void testInitAPIAccessError1() { | ||||||
|         final MoodleRestTemplateFactory moodleRestTemplateFactory = mock(MoodleRestTemplateFactory.class); |         final MoodleRestTemplateFactoryImpl moodleRestTemplateFactory = mock(MoodleRestTemplateFactoryImpl.class); | ||||||
|         when(moodleRestTemplateFactory.createRestTemplate()).thenReturn(Result.ofRuntimeError("Error1")); |         when(moodleRestTemplateFactory.createRestTemplate()).thenReturn(Result.ofRuntimeError("Error1")); | ||||||
|         when(moodleRestTemplateFactory.test()).thenReturn(LmsSetupTestResult.ofOkay(LmsType.MOODLE)); |         when(moodleRestTemplateFactory.test()).thenReturn(LmsSetupTestResult.ofOkay(LmsType.MOODLE)); | ||||||
| 
 | 
 | ||||||
|  | @ -138,7 +138,7 @@ public class MoodleCourseAccessTest { | ||||||
| 
 | 
 | ||||||
|     @Test |     @Test | ||||||
|     public void testInitAPIAccessError2() { |     public void testInitAPIAccessError2() { | ||||||
|         final MoodleRestTemplateFactory moodleRestTemplateFactory = mock(MoodleRestTemplateFactory.class); |         final MoodleRestTemplateFactoryImpl moodleRestTemplateFactory = mock(MoodleRestTemplateFactoryImpl.class); | ||||||
|         final MoodleAPIRestTemplateImpl moodleAPIRestTemplate = mock(MoodleAPIRestTemplateImpl.class); |         final MoodleAPIRestTemplateImpl moodleAPIRestTemplate = mock(MoodleAPIRestTemplateImpl.class); | ||||||
|         when(moodleRestTemplateFactory.createRestTemplate()).thenReturn(Result.of(moodleAPIRestTemplate)); |         when(moodleRestTemplateFactory.createRestTemplate()).thenReturn(Result.of(moodleAPIRestTemplate)); | ||||||
|         doThrow(RuntimeException.class).when(moodleAPIRestTemplate).testAPIConnection(any()); |         doThrow(RuntimeException.class).when(moodleAPIRestTemplate).testAPIConnection(any()); | ||||||
|  | @ -160,7 +160,7 @@ public class MoodleCourseAccessTest { | ||||||
| 
 | 
 | ||||||
|     @Test |     @Test | ||||||
|     public void testInitAPIAccessOK() { |     public void testInitAPIAccessOK() { | ||||||
|         final MoodleRestTemplateFactory moodleRestTemplateFactory = mock(MoodleRestTemplateFactory.class); |         final MoodleRestTemplateFactoryImpl moodleRestTemplateFactory = mock(MoodleRestTemplateFactoryImpl.class); | ||||||
|         final MoodleAPIRestTemplateImpl moodleAPIRestTemplate = mock(MoodleAPIRestTemplateImpl.class); |         final MoodleAPIRestTemplateImpl moodleAPIRestTemplate = mock(MoodleAPIRestTemplateImpl.class); | ||||||
|         when(moodleRestTemplateFactory.createRestTemplate()).thenReturn(Result.of(moodleAPIRestTemplate)); |         when(moodleRestTemplateFactory.createRestTemplate()).thenReturn(Result.of(moodleAPIRestTemplate)); | ||||||
|         when(moodleRestTemplateFactory.test()).thenReturn(LmsSetupTestResult.ofOkay(LmsType.MOODLE)); |         when(moodleRestTemplateFactory.test()).thenReturn(LmsSetupTestResult.ofOkay(LmsType.MOODLE)); | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		
		Reference in a new issue
	
	 anhefti
						anhefti