refactoring of LMS API service with overall eh-caching
This commit is contained in:
		
							parent
							
								
									dd6150ec2a
								
							
						
					
					
						commit
						9214719642
					
				
					 19 changed files with 406 additions and 210 deletions
				
			
		|  | @ -59,6 +59,7 @@ public final class Constants { | ||||||
|     public static final Character DOUBLE_QUOTE = '"'; |     public static final Character DOUBLE_QUOTE = '"'; | ||||||
|     public static final Character COMMA = ','; |     public static final Character COMMA = ','; | ||||||
|     public static final Character PIPE = '|'; |     public static final Character PIPE = '|'; | ||||||
|  |     public static final Character UNDERLINE = '_'; | ||||||
|     public static final Character AMPERSAND = '&'; |     public static final Character AMPERSAND = '&'; | ||||||
|     public static final Character EQUALITY_SIGN = '='; |     public static final Character EQUALITY_SIGN = '='; | ||||||
|     public static final Character LIST_SEPARATOR_CHAR = COMMA; |     public static final Character LIST_SEPARATOR_CHAR = COMMA; | ||||||
|  |  | ||||||
|  | @ -37,6 +37,8 @@ public interface ExamDAO extends ActivatableEntityDAO<Exam, Exam>, BulkActionSup | ||||||
|      *         happened */ |      *         happened */ | ||||||
|     Result<GrantEntity> examGrantEntityByClientConnection(Long connectionId); |     Result<GrantEntity> examGrantEntityByClientConnection(Long connectionId); | ||||||
| 
 | 
 | ||||||
|  |     Result<Exam> getWithQuizDataFromCache(Long id); | ||||||
|  | 
 | ||||||
|     /** Get all active Exams for a given institution. |     /** Get all active Exams for a given institution. | ||||||
|      * |      * | ||||||
|      * @param institutionId the identifier of the institution |      * @param institutionId the identifier of the institution | ||||||
|  |  | ||||||
|  | @ -377,6 +377,7 @@ public class ExamConfigurationMapDAOImpl implements ExamConfigurationMapDAO { | ||||||
| 
 | 
 | ||||||
|     private Result<Collection<ExamConfigurationMap>> toDomainModel( |     private Result<Collection<ExamConfigurationMap>> toDomainModel( | ||||||
|             final Collection<ExamConfigurationMapRecord> records) { |             final Collection<ExamConfigurationMapRecord> records) { | ||||||
|  | 
 | ||||||
|         return Result.tryCatch(() -> records |         return Result.tryCatch(() -> records | ||||||
|                 .stream() |                 .stream() | ||||||
|                 .map(model -> this.toDomainModel(model).getOrThrow()) |                 .map(model -> this.toDomainModel(model).getOrThrow()) | ||||||
|  | @ -390,7 +391,8 @@ public class ExamConfigurationMapDAOImpl implements ExamConfigurationMapDAO { | ||||||
|                     .selectByPrimaryKey(record.getConfigurationNodeId()); |                     .selectByPrimaryKey(record.getConfigurationNodeId()); | ||||||
|             final String status = config.getStatus(); |             final String status = config.getStatus(); | ||||||
| 
 | 
 | ||||||
|             final Exam exam = this.examDAO.byPK(record.getExamId()) |             final Exam exam = this.examDAO | ||||||
|  |                     .getWithQuizDataFromCache(record.getExamId()) | ||||||
|                     .getOr(null); |                     .getOr(null); | ||||||
| 
 | 
 | ||||||
|             return new ExamConfigurationMap( |             return new ExamConfigurationMap( | ||||||
|  |  | ||||||
|  | @ -119,6 +119,13 @@ public class ExamDAOImpl implements ExamDAO { | ||||||
|                 .map(record -> toDomainModel(record, null, null).getOrThrow()); |                 .map(record -> toDomainModel(record, null, null).getOrThrow()); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     @Override | ||||||
|  |     @Transactional(readOnly = true) | ||||||
|  |     public Result<Exam> getWithQuizDataFromCache(final Long id) { | ||||||
|  |         return recordById(id) | ||||||
|  |                 .flatMap(this::toDomainModelFromCache); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     @Override |     @Override | ||||||
|     @Transactional(readOnly = true) |     @Transactional(readOnly = true) | ||||||
|     public Result<Collection<Exam>> all(final Long institutionId, final Boolean active) { |     public Result<Collection<Exam>> all(final Long institutionId, final Boolean active) { | ||||||
|  | @ -761,6 +768,17 @@ public class ExamDAOImpl implements ExamDAO { | ||||||
|                 exam.getDescription()); |                 exam.getDescription()); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     private Result<Exam> toDomainModelFromCache(final ExamRecord record) { | ||||||
|  | 
 | ||||||
|  |         return this.lmsAPIService | ||||||
|  |                 .getLmsAPITemplate(record.getLmsSetupId()) | ||||||
|  |                 .flatMap(template -> this.toDomainModel( | ||||||
|  |                         record, | ||||||
|  |                         template.getQuizFromCache(record.getExternalId()) | ||||||
|  |                                 .getOrThrow(), | ||||||
|  |                         null)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     private Result<Exam> toDomainModel(final ExamRecord record) { |     private Result<Exam> toDomainModel(final ExamRecord record) { | ||||||
|         return toDomainModel( |         return toDomainModel( | ||||||
|                 record.getLmsSetupId(), |                 record.getLmsSetupId(), | ||||||
|  |  | ||||||
|  | @ -165,6 +165,15 @@ public interface LmsAPITemplate { | ||||||
|      * @return Collection of all {@link QuizData } from the given id set */ |      * @return Collection of all {@link QuizData } from the given id set */ | ||||||
|     Collection<Result<QuizData>> getQuizzesFromCache(Set<String> ids); |     Collection<Result<QuizData>> getQuizzesFromCache(Set<String> ids); | ||||||
| 
 | 
 | ||||||
|  |     /** Get a particular quiz data from cache if available. If not, tries to get it from the LMS. | ||||||
|  |      * | ||||||
|  |      * @param id the quiz identifier, external identifier of the exam. | ||||||
|  |      * @return Result refer to the {@link QuizData } or to an error when happended */ | ||||||
|  |     Result<QuizData> getQuizFromCache(String id); | ||||||
|  | 
 | ||||||
|  |     /** Clears the underling caches if there are some for a particular implementation. */ | ||||||
|  |     void clearCache(); | ||||||
|  | 
 | ||||||
|     /** Convert an anonymous or temporary examineeUserId, sent by the SEB Client on LMS login, |     /** Convert an anonymous or temporary examineeUserId, sent by the SEB Client on LMS login, | ||||||
|      * to LMS examinee account details by requesting them on the LMS API with the given examineeUserId |      * to LMS examinee account details by requesting them on the LMS API with the given examineeUserId | ||||||
|      * |      * | ||||||
|  |  | ||||||
|  | @ -0,0 +1,82 @@ | ||||||
|  | /* | ||||||
|  |  * Copyright (c) 2021 ETH Zürich, Educational Development and Technology (LET) | ||||||
|  |  * | ||||||
|  |  * This Source Code Form is subject to the terms of the Mozilla Public | ||||||
|  |  * License, v. 2.0. If a copy of the MPL was not distributed with this | ||||||
|  |  * file, You can obtain one at http://mozilla.org/MPL/2.0/. | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl; | ||||||
|  | 
 | ||||||
|  | import java.util.Collection; | ||||||
|  | import java.util.Set; | ||||||
|  | import java.util.stream.Collectors; | ||||||
|  | 
 | ||||||
|  | import org.springframework.cache.Cache; | ||||||
|  | import org.springframework.cache.CacheManager; | ||||||
|  | import org.springframework.core.env.Environment; | ||||||
|  | 
 | ||||||
|  | import ch.ethz.seb.sebserver.gbl.Constants; | ||||||
|  | import ch.ethz.seb.sebserver.gbl.async.AsyncService; | ||||||
|  | import ch.ethz.seb.sebserver.gbl.model.exam.QuizData; | ||||||
|  | import ch.ethz.seb.sebserver.gbl.util.Result; | ||||||
|  | 
 | ||||||
|  | public abstract class AbstractCachedCourseAccess extends AbstractCourseAccess { | ||||||
|  | 
 | ||||||
|  |     public static final String CACHE_NAME_QUIZ_DATA = "QUIZ_DATA_CACHE"; | ||||||
|  | 
 | ||||||
|  |     private static final String NO_QUIZ_DATA_FOUND_ERROR = "NO_QUIZ_DATA_FOUND_ERROR"; | ||||||
|  | 
 | ||||||
|  |     private final Cache cache; | ||||||
|  | 
 | ||||||
|  |     protected AbstractCachedCourseAccess( | ||||||
|  |             final AsyncService asyncService, | ||||||
|  |             final Environment environment, | ||||||
|  |             final CacheManager cacheManager) { | ||||||
|  | 
 | ||||||
|  |         super(asyncService, environment); | ||||||
|  |         this.cache = cacheManager.getCache(CACHE_NAME_QUIZ_DATA); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void clearCache() { | ||||||
|  |         this.cache.clear(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     protected QuizData getFromCache(final String id) { | ||||||
|  |         return this.cache.get(createCacheKey(id), QuizData.class); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     protected void putToCache(final QuizData quizData) { | ||||||
|  |         this.cache.put(createCacheKey(quizData.id), quizData); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     protected void putToCache(final Collection<QuizData> quizData) { | ||||||
|  |         quizData.stream().forEach(q -> this.cache.put(createCacheKey(q.id), q)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     protected void evict(final String id) { | ||||||
|  |         this.cache.evict(createCacheKey(id)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public Result<QuizData> getQuizFromCache(final String id) { | ||||||
|  |         final QuizData fromCache = getFromCache(id); | ||||||
|  |         if (fromCache != null) { | ||||||
|  |             return Result.of(fromCache); | ||||||
|  |         } else { | ||||||
|  |             return Result.ofRuntimeError(NO_QUIZ_DATA_FOUND_ERROR); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public Result<Collection<Result<QuizData>>> getQuizzesFromCache(final Set<String> ids) { | ||||||
|  |         return Result.of(ids.stream().map(this::getQuizFromCache).collect(Collectors.toList())); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     protected abstract Long getLmsSetupId(); | ||||||
|  | 
 | ||||||
|  |     private final String createCacheKey(final String id) { | ||||||
|  |         return id + Constants.UNDERLINE + getLmsSetupId(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -11,12 +11,8 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl; | ||||||
| import java.util.Collection; | import java.util.Collection; | ||||||
| import java.util.Collections; | import java.util.Collections; | ||||||
| import java.util.List; | import java.util.List; | ||||||
| import java.util.Map; |  | ||||||
| import java.util.NoSuchElementException; |  | ||||||
| import java.util.Set; | import java.util.Set; | ||||||
| import java.util.function.Function; |  | ||||||
| import java.util.function.Supplier; | import java.util.function.Supplier; | ||||||
| import java.util.stream.Collectors; |  | ||||||
| 
 | 
 | ||||||
| import org.slf4j.Logger; | import org.slf4j.Logger; | ||||||
| import org.slf4j.LoggerFactory; | import org.slf4j.LoggerFactory; | ||||||
|  | @ -25,6 +21,7 @@ import org.springframework.core.env.Environment; | ||||||
| import ch.ethz.seb.sebserver.gbl.Constants; | import ch.ethz.seb.sebserver.gbl.Constants; | ||||||
| import ch.ethz.seb.sebserver.gbl.async.AsyncService; | import ch.ethz.seb.sebserver.gbl.async.AsyncService; | ||||||
| import ch.ethz.seb.sebserver.gbl.async.CircuitBreaker; | import ch.ethz.seb.sebserver.gbl.async.CircuitBreaker; | ||||||
|  | import ch.ethz.seb.sebserver.gbl.async.CircuitBreaker.State; | ||||||
| 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.user.ExamineeAccountDetails; | import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails; | ||||||
|  | @ -101,42 +98,8 @@ public abstract class AbstractCourseAccess { | ||||||
|                         Constants.SECOND_IN_MILLIS * 10)); |                         Constants.SECOND_IN_MILLIS * 10)); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public Result<Collection<Result<QuizData>>> getQuizzesFromCache(final Set<String> ids) { |  | ||||||
|         return Result.tryCatch(() -> { |  | ||||||
|             final List<QuizData> cached = allQuizzesSupplier().getAllCached(); |  | ||||||
|             final List<QuizData> available = (cached != null) |  | ||||||
|                     ? cached |  | ||||||
|                     : Collections.emptyList(); |  | ||||||
| 
 |  | ||||||
|             final Map<String, QuizData> quizMapping = available |  | ||||||
|                     .stream() |  | ||||||
|                     .collect(Collectors.toMap(q -> q.id, Function.identity())); |  | ||||||
| 
 |  | ||||||
|             if (!quizMapping.keySet().containsAll(ids)) { |  | ||||||
| 
 |  | ||||||
|                 final Map<String, QuizData> collect = quizzesSupplier(ids).get() |  | ||||||
|                         .stream() |  | ||||||
|                         .collect(Collectors.toMap(qd -> qd.id, Function.identity())); |  | ||||||
|                 if (collect != null) { |  | ||||||
|                     quizMapping.clear(); |  | ||||||
|                     quizMapping.putAll(collect); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             return ids |  | ||||||
|                     .stream() |  | ||||||
|                     .map(id -> { |  | ||||||
|                         final QuizData q = quizMapping.get(id); |  | ||||||
|                         return (q == null) |  | ||||||
|                                 ? Result.<QuizData> ofError(new NoSuchElementException("Quiz with id: " + id)) |  | ||||||
|                                 : Result.of(q); |  | ||||||
|                     }) |  | ||||||
|                     .collect(Collectors.toList()); |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public Result<List<QuizData>> getQuizzes(final FilterMap filterMap) { |     public Result<List<QuizData>> getQuizzes(final FilterMap filterMap) { | ||||||
|         return allQuizzesSupplier().getAll(filterMap); |         return this.quizzesRequest.protectedRun(allQuizzesSupplier(filterMap)); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeSessionId) { |     public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeSessionId) { | ||||||
|  | @ -172,30 +135,41 @@ public abstract class AbstractCourseAccess { | ||||||
|                 Collections.emptyMap()); |                 Collections.emptyMap()); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** This abstraction has no cache implementation and therefore this returns a Result | ||||||
|  |      * with an "No cache supported error. | ||||||
|  |      * </p> | ||||||
|  |      * To implement and use caching, this must be overridden and implemented | ||||||
|  |      * | ||||||
|  |      * @param id The identifier of the QuizData to get from cache | ||||||
|  |      * @return Result with an "No cache supported error */ | ||||||
|  |     public Result<QuizData> getQuizFromCache(final String id) { | ||||||
|  |         return Result.ofRuntimeError("No cache supported"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** This abstraction has no cache implementation and therefore this returns a Result | ||||||
|  |      * with an "No cache supported error. | ||||||
|  |      * </p> | ||||||
|  |      * To implement and use caching, this must be overridden and implemented | ||||||
|  |      * | ||||||
|  |      * @param ids Collection of quiz data identifier to get from the cache | ||||||
|  |      * @return Result with an "No cache supported error */ | ||||||
|  |     public Result<Collection<Result<QuizData>>> getQuizzesFromCache(final Set<String> ids) { | ||||||
|  |         return Result.ofRuntimeError("No cache supported"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /** Provides a supplier for the quiz data request to use within the circuit breaker */ |     /** Provides a supplier for the quiz data request to use within the circuit breaker */ | ||||||
|     protected abstract Supplier<List<QuizData>> quizzesSupplier(final Set<String> ids); |     protected abstract Supplier<List<QuizData>> quizzesSupplier(final Set<String> ids); | ||||||
| 
 | 
 | ||||||
|     /** Provides a AllQuizzesSupplier to supply quiz data either form cache or from LMS */ |     /** Provides a supplier to supply request to use within the circuit breaker */ | ||||||
|     protected abstract AllQuizzesSupplier allQuizzesSupplier(); |     protected abstract Supplier<List<QuizData>> allQuizzesSupplier(final FilterMap filterMap); | ||||||
| 
 | 
 | ||||||
|     /** Provides a supplier for the course chapter data request to use within the circuit breaker */ |     /** Provides a supplier for the course chapter data request to use within the circuit breaker */ | ||||||
|     protected abstract Supplier<Chapters> getCourseChaptersSupplier(final String courseId); |     protected abstract Supplier<Chapters> getCourseChaptersSupplier(final String courseId); | ||||||
| 
 | 
 | ||||||
|     /** Gives a fetch status if asynchronous quiz data fetching is part of the underling implementation */ |     protected FetchStatus getFetchStatus() { | ||||||
|     protected abstract FetchStatus getFetchStatus(); |         if (this.quizzesRequest.getState() != State.CLOSED) { | ||||||
| 
 |             return FetchStatus.FETCH_ERROR; | ||||||
|     /** Uses to supply quiz data */ |         } | ||||||
|     protected interface AllQuizzesSupplier { |         return FetchStatus.ALL_FETCHED; | ||||||
|         /** Get all currently cached quiz data if supported by the underling implementation |  | ||||||
|          * |  | ||||||
|          * @return List containing all cached quiz data objects */ |  | ||||||
|         List<QuizData> getAllCached(); |  | ||||||
| 
 |  | ||||||
|         /** Get a list of all quiz data filtered by the given filter map from LMS. |  | ||||||
|          * |  | ||||||
|          * @param filterMap Map containing the filter criteria |  | ||||||
|          * @return Result refer to the list of filtered quiz data or to an error when happened */ |  | ||||||
|         Result<List<QuizData>> getAll(final FilterMap filterMap); |  | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -98,17 +98,20 @@ public class LmsAPIServiceImpl implements LmsAPIService { | ||||||
|     @Override |     @Override | ||||||
|     public Result<LmsAPITemplate> getLmsAPITemplate(final String lmsSetupId) { |     public Result<LmsAPITemplate> getLmsAPITemplate(final String lmsSetupId) { | ||||||
|         return Result.tryCatch(() -> { |         return Result.tryCatch(() -> { | ||||||
|             LmsAPITemplate lmsAPITemplate = getFromCache(lmsSetupId); |             synchronized (this) { | ||||||
|             if (lmsAPITemplate == null) { |                 LmsAPITemplate lmsAPITemplate = getFromCache(lmsSetupId); | ||||||
|                 lmsAPITemplate = createLmsSetupTemplate(lmsSetupId); |                 if (lmsAPITemplate == null) { | ||||||
|                 if (lmsAPITemplate != null) { |                     lmsAPITemplate = createLmsSetupTemplate(lmsSetupId); | ||||||
|                     this.cache.put(new CacheKey(lmsSetupId, System.currentTimeMillis()), lmsAPITemplate); |                     if (lmsAPITemplate != null) { | ||||||
|  |                         this.cache.put(new CacheKey(lmsSetupId, System.currentTimeMillis()), lmsAPITemplate); | ||||||
|  |                     } | ||||||
|                 } |                 } | ||||||
|  |                 if (lmsAPITemplate == null) { | ||||||
|  |                     throw new ResourceNotFoundException(EntityType.LMS_SETUP, lmsSetupId); | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 return lmsAPITemplate; | ||||||
|             } |             } | ||||||
|             if (lmsAPITemplate == null) { |  | ||||||
|                 throw new ResourceNotFoundException(EntityType.LMS_SETUP, lmsSetupId); |  | ||||||
|             } |  | ||||||
|             return lmsAPITemplate; |  | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -91,12 +91,10 @@ public class SEBRestrictionServiceImpl implements SEBRestrictionService { | ||||||
|     public Result<SEBRestriction> getSEBRestrictionFromExam(final Exam exam) { |     public Result<SEBRestriction> getSEBRestrictionFromExam(final Exam exam) { | ||||||
|         return Result.tryCatch(() -> { |         return Result.tryCatch(() -> { | ||||||
|             // load the config keys from restriction and merge with new generated config keys |             // load the config keys from restriction and merge with new generated config keys | ||||||
|             final long currentTimeMillis = System.currentTimeMillis(); |  | ||||||
|             final Set<String> configKeys = new HashSet<>(); |             final Set<String> configKeys = new HashSet<>(); | ||||||
|             final Collection<String> generatedKeys = this.examConfigService |             final Collection<String> generatedKeys = this.examConfigService | ||||||
|                     .generateConfigKeys(exam.institutionId, exam.id) |                     .generateConfigKeys(exam.institutionId, exam.id) | ||||||
|                     .getOrThrow(); |                     .getOrThrow(); | ||||||
|             System.out.println("******* " + (System.currentTimeMillis() - currentTimeMillis)); |  | ||||||
| 
 | 
 | ||||||
|             configKeys.addAll(generatedKeys); |             configKeys.addAll(generatedKeys); | ||||||
|             if (generatedKeys != null && !generatedKeys.isEmpty()) { |             if (generatedKeys != null && !generatedKeys.isEmpty()) { | ||||||
|  |  | ||||||
|  | @ -21,6 +21,7 @@ import java.util.stream.Collectors; | ||||||
| import org.apache.commons.lang3.StringUtils; | import org.apache.commons.lang3.StringUtils; | ||||||
| import org.slf4j.Logger; | import org.slf4j.Logger; | ||||||
| import org.slf4j.LoggerFactory; | import org.slf4j.LoggerFactory; | ||||||
|  | import org.springframework.cache.CacheManager; | ||||||
| import org.springframework.core.env.Environment; | import org.springframework.core.env.Environment; | ||||||
| import org.springframework.http.HttpEntity; | import org.springframework.http.HttpEntity; | ||||||
| import org.springframework.http.HttpHeaders; | import org.springframework.http.HttpHeaders; | ||||||
|  | @ -43,8 +44,6 @@ import com.fasterxml.jackson.core.type.TypeReference; | ||||||
| import ch.ethz.seb.sebserver.gbl.Constants; | import ch.ethz.seb.sebserver.gbl.Constants; | ||||||
| import ch.ethz.seb.sebserver.gbl.api.JSONMapper; | import ch.ethz.seb.sebserver.gbl.api.JSONMapper; | ||||||
| import ch.ethz.seb.sebserver.gbl.async.AsyncService; | import ch.ethz.seb.sebserver.gbl.async.AsyncService; | ||||||
| import ch.ethz.seb.sebserver.gbl.async.CircuitBreaker.State; |  | ||||||
| import ch.ethz.seb.sebserver.gbl.async.MemoizingCircuitBreaker; |  | ||||||
| import ch.ethz.seb.sebserver.gbl.model.exam.Chapters; | import ch.ethz.seb.sebserver.gbl.model.exam.Chapters; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.exam.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; | ||||||
|  | @ -55,12 +54,12 @@ import ch.ethz.seb.sebserver.gbl.util.Utils; | ||||||
| import ch.ethz.seb.sebserver.webservice.WebserviceInfo; | import ch.ethz.seb.sebserver.webservice.WebserviceInfo; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; | import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier; | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.AbstractCourseAccess; | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.AbstractCachedCourseAccess; | ||||||
| 
 | 
 | ||||||
| /** Implements the LmsAPITemplate for Open edX LMS Course API access. | /** Implements the LmsAPITemplate for Open edX LMS Course API access. | ||||||
|  * |  * | ||||||
|  * See also: https://course-catalog-api-guide.readthedocs.io */ |  * See also: https://course-catalog-api-guide.readthedocs.io */ | ||||||
| final class OpenEdxCourseAccess extends AbstractCourseAccess { | final class OpenEdxCourseAccess extends AbstractCachedCourseAccess { | ||||||
| 
 | 
 | ||||||
|     private static final Logger log = LoggerFactory.getLogger(OpenEdxCourseAccess.class); |     private static final Logger log = LoggerFactory.getLogger(OpenEdxCourseAccess.class); | ||||||
| 
 | 
 | ||||||
|  | @ -74,65 +73,34 @@ final class OpenEdxCourseAccess extends AbstractCourseAccess { | ||||||
|     private final JSONMapper jsonMapper; |     private final JSONMapper jsonMapper; | ||||||
|     private final OpenEdxRestTemplateFactory openEdxRestTemplateFactory; |     private final OpenEdxRestTemplateFactory openEdxRestTemplateFactory; | ||||||
|     private final WebserviceInfo webserviceInfo; |     private final WebserviceInfo webserviceInfo; | ||||||
|     private final MemoizingCircuitBreaker<List<QuizData>> allQuizzesRequest; |  | ||||||
|     private final AllQuizzesSupplier allQuizzesSupplier; |  | ||||||
| 
 | 
 | ||||||
|     private OAuth2RestTemplate restTemplate; |     private OAuth2RestTemplate restTemplate; | ||||||
|  |     private final Long lmsSetupId; | ||||||
| 
 | 
 | ||||||
|     public OpenEdxCourseAccess( |     public OpenEdxCourseAccess( | ||||||
|             final JSONMapper jsonMapper, |             final JSONMapper jsonMapper, | ||||||
|             final OpenEdxRestTemplateFactory openEdxRestTemplateFactory, |             final OpenEdxRestTemplateFactory openEdxRestTemplateFactory, | ||||||
|             final WebserviceInfo webserviceInfo, |             final WebserviceInfo webserviceInfo, | ||||||
|             final AsyncService asyncService, |             final AsyncService asyncService, | ||||||
|             final Environment environment) { |             final Environment environment, | ||||||
|  |             final CacheManager cacheManager) { | ||||||
| 
 | 
 | ||||||
|         super(asyncService, environment); |         super(asyncService, environment, cacheManager); | ||||||
|         this.jsonMapper = jsonMapper; |         this.jsonMapper = jsonMapper; | ||||||
|         this.openEdxRestTemplateFactory = openEdxRestTemplateFactory; |         this.openEdxRestTemplateFactory = openEdxRestTemplateFactory; | ||||||
|         this.webserviceInfo = webserviceInfo; |         this.webserviceInfo = webserviceInfo; | ||||||
| 
 |         this.lmsSetupId = openEdxRestTemplateFactory.apiTemplateDataSupplier.getLmsSetup().id; | ||||||
|         this.allQuizzesRequest = asyncService.createMemoizingCircuitBreaker( |  | ||||||
|                 quizzesSupplier(), |  | ||||||
|                 environment.getProperty( |  | ||||||
|                         "sebserver.webservice.circuitbreaker.allQuizzesRequest.attempts", |  | ||||||
|                         Integer.class, |  | ||||||
|                         3), |  | ||||||
|                 environment.getProperty( |  | ||||||
|                         "sebserver.webservice.circuitbreaker.allQuizzesRequest.blockingTime", |  | ||||||
|                         Long.class, |  | ||||||
|                         Constants.MINUTE_IN_MILLIS), |  | ||||||
|                 environment.getProperty( |  | ||||||
|                         "sebserver.webservice.circuitbreaker.allQuizzesRequest.timeToRecover", |  | ||||||
|                         Long.class, |  | ||||||
|                         Constants.MINUTE_IN_MILLIS), |  | ||||||
|                 environment.getProperty( |  | ||||||
|                         "sebserver.webservice.circuitbreaker.allQuizzesRequest.memoize", |  | ||||||
|                         Boolean.class, |  | ||||||
|                         true), |  | ||||||
|                 environment.getProperty( |  | ||||||
|                         "sebserver.webservice.circuitbreaker.allQuizzesRequest.memoizingTime", |  | ||||||
|                         Long.class, |  | ||||||
|                         Constants.HOUR_IN_MILLIS)); |  | ||||||
| 
 |  | ||||||
|         this.allQuizzesSupplier = new AllQuizzesSupplier() { |  | ||||||
| 
 |  | ||||||
|             @Override |  | ||||||
|             public List<QuizData> getAllCached() { |  | ||||||
|                 return OpenEdxCourseAccess.this.allQuizzesRequest.getCached(); |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             @Override |  | ||||||
|             public Result<List<QuizData>> getAll(final FilterMap filterMap) { |  | ||||||
|                 return OpenEdxCourseAccess.this.allQuizzesRequest.get(); |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|         }; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     APITemplateDataSupplier getApiTemplateDataSupplier() { |     APITemplateDataSupplier getApiTemplateDataSupplier() { | ||||||
|         return this.openEdxRestTemplateFactory.apiTemplateDataSupplier; |         return this.openEdxRestTemplateFactory.apiTemplateDataSupplier; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     @Override | ||||||
|  |     protected Long getLmsSetupId() { | ||||||
|  |         return this.lmsSetupId; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     LmsSetupTestResult initAPIAccess() { |     LmsSetupTestResult initAPIAccess() { | ||||||
| 
 | 
 | ||||||
|         final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup(); |         final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup(); | ||||||
|  | @ -219,6 +187,13 @@ final class OpenEdxCourseAccess extends AbstractCourseAccess { | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     @Override | ||||||
|  |     protected Supplier<List<QuizData>> allQuizzesSupplier(final FilterMap filterMap) { | ||||||
|  |         return () -> getRestTemplate() | ||||||
|  |                 .map(this::collectAllQuizzes) | ||||||
|  |                 .getOrThrow(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     @Override |     @Override | ||||||
|     protected Supplier<List<QuizData>> quizzesSupplier(final Set<String> ids) { |     protected Supplier<List<QuizData>> quizzesSupplier(final Set<String> ids) { | ||||||
| 
 | 
 | ||||||
|  | @ -231,7 +206,7 @@ final class OpenEdxCourseAccess extends AbstractCourseAccess { | ||||||
|                         getOneCourses( |                         getOneCourses( | ||||||
|                                 lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_ENDPOINT, |                                 lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_ENDPOINT, | ||||||
|                                 getRestTemplate().getOrThrow(), |                                 getRestTemplate().getOrThrow(), | ||||||
|                                 ids.iterator().next()), |                                 ids), | ||||||
|                         externalStartURI)); |                         externalStartURI)); | ||||||
|             }; |             }; | ||||||
|         } else { |         } else { | ||||||
|  | @ -241,12 +216,6 @@ final class OpenEdxCourseAccess extends AbstractCourseAccess { | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private Supplier<List<QuizData>> quizzesSupplier() { |  | ||||||
|         return () -> getRestTemplate() |  | ||||||
|                 .map(this::collectAllQuizzes) |  | ||||||
|                 .getOrThrow(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |     @Override | ||||||
|     protected Supplier<Chapters> getCourseChaptersSupplier(final String courseId) { |     protected Supplier<Chapters> getCourseChaptersSupplier(final String courseId) { | ||||||
|         return () -> { |         return () -> { | ||||||
|  | @ -337,7 +306,10 @@ final class OpenEdxCourseAccess extends AbstractCourseAccess { | ||||||
|         final List<CourseData> collector = new ArrayList<>(); |         final List<CourseData> collector = new ArrayList<>(); | ||||||
|         EdXPage page = getEdxPage(pageURI, restTemplate).getBody(); |         EdXPage page = getEdxPage(pageURI, restTemplate).getBody(); | ||||||
|         if (page != null) { |         if (page != null) { | ||||||
|             collector.addAll(page.results); |             page.results | ||||||
|  |                     .stream() | ||||||
|  |                     .filter(cd -> ids.contains(cd.id)) | ||||||
|  |                     .forEach(collector::add); | ||||||
|             while (page != null && StringUtils.isNotBlank(page.next)) { |             while (page != null && StringUtils.isNotBlank(page.next)) { | ||||||
|                 page = getEdxPage(page.next, restTemplate).getBody(); |                 page = getEdxPage(page.next, restTemplate).getBody(); | ||||||
|                 if (page != null) { |                 if (page != null) { | ||||||
|  | @ -355,16 +327,33 @@ final class OpenEdxCourseAccess extends AbstractCourseAccess { | ||||||
|     private CourseData getOneCourses( |     private CourseData getOneCourses( | ||||||
|             final String pageURI, |             final String pageURI, | ||||||
|             final OAuth2RestTemplate restTemplate, |             final OAuth2RestTemplate restTemplate, | ||||||
|             final String id) { |             final Set<String> ids) { | ||||||
| 
 | 
 | ||||||
|         final HttpHeaders httpHeaders = new HttpHeaders(); |         System.out.println("********************"); | ||||||
|         final ResponseEntity<CourseData> exchange = restTemplate.exchange( |  | ||||||
|                 pageURI + "/" + id, |  | ||||||
|                 HttpMethod.GET, |  | ||||||
|                 new HttpEntity<>(httpHeaders), |  | ||||||
|                 CourseData.class); |  | ||||||
| 
 | 
 | ||||||
|         return exchange.getBody(); |         // NOTE: try first to get the course data by id. This seems to be possible | ||||||
|  |         // when the SEB restriction is not set. Once the SEB restriction is set, | ||||||
|  |         // this gives a 403 response. | ||||||
|  |         // We haven't found another way to get course data by id in this case so far | ||||||
|  |         // Workaround is to search the course by paging (slow) | ||||||
|  |         try { | ||||||
|  |             final HttpHeaders httpHeaders = new HttpHeaders(); | ||||||
|  |             final String uri = pageURI + ids.iterator().next(); | ||||||
|  |             final ResponseEntity<CourseData> exchange = restTemplate.exchange( | ||||||
|  |                     uri, | ||||||
|  |                     HttpMethod.GET, | ||||||
|  |                     new HttpEntity<>(httpHeaders), | ||||||
|  |                     CourseData.class); | ||||||
|  | 
 | ||||||
|  |             return exchange.getBody(); | ||||||
|  |         } catch (final Exception e) { | ||||||
|  |             // try with paging | ||||||
|  |             final List<CourseData> collectCourses = collectCourses(pageURI, restTemplate, ids); | ||||||
|  |             if (collectCourses.isEmpty()) { | ||||||
|  |                 return null; | ||||||
|  |             } | ||||||
|  |             return collectCourses.get(0); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private List<CourseData> collectAllCourses(final String pageURI, final OAuth2RestTemplate restTemplate) { |     private List<CourseData> collectAllCourses(final String pageURI, final OAuth2RestTemplate restTemplate) { | ||||||
|  | @ -403,7 +392,7 @@ final class OpenEdxCourseAccess extends AbstractCourseAccess { | ||||||
|                         Blocks.class); |                         Blocks.class); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static QuizData quizDataOf( |     private QuizData quizDataOf( | ||||||
|             final LmsSetup lmsSetup, |             final LmsSetup lmsSetup, | ||||||
|             final CourseData courseData, |             final CourseData courseData, | ||||||
|             final String uriPrefix) { |             final String uriPrefix) { | ||||||
|  | @ -411,7 +400,7 @@ final class OpenEdxCourseAccess extends AbstractCourseAccess { | ||||||
|         final String startURI = uriPrefix + courseData.id; |         final String startURI = uriPrefix + courseData.id; | ||||||
|         final Map<String, String> additionalAttrs = new HashMap<>(); |         final Map<String, String> additionalAttrs = new HashMap<>(); | ||||||
|         additionalAttrs.put("blocks_url", courseData.blocks_url); |         additionalAttrs.put("blocks_url", courseData.blocks_url); | ||||||
|         return new QuizData( |         final QuizData quizData = new QuizData( | ||||||
|                 courseData.id, |                 courseData.id, | ||||||
|                 lmsSetup.getInstitutionId(), |                 lmsSetup.getInstitutionId(), | ||||||
|                 lmsSetup.id, |                 lmsSetup.id, | ||||||
|  | @ -421,6 +410,10 @@ final class OpenEdxCourseAccess extends AbstractCourseAccess { | ||||||
|                 courseData.start, |                 courseData.start, | ||||||
|                 courseData.end, |                 courseData.end, | ||||||
|                 startURI); |                 startURI); | ||||||
|  | 
 | ||||||
|  |         super.putToCache(quizData); | ||||||
|  | 
 | ||||||
|  |         return quizData; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** Maps a OpenEdX course API course page */ |     /** Maps a OpenEdX course API course page */ | ||||||
|  | @ -538,17 +531,4 @@ final class OpenEdxCourseAccess extends AbstractCourseAccess { | ||||||
|         return Result.of(this.restTemplate); |         return Result.of(this.restTemplate); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |  | ||||||
|     protected FetchStatus getFetchStatus() { |  | ||||||
|         if (this.allQuizzesRequest.getState() != State.CLOSED) { |  | ||||||
|             return FetchStatus.FETCH_ERROR; |  | ||||||
|         } |  | ||||||
|         return FetchStatus.ALL_FETCHED; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     protected AllQuizzesSupplier allQuizzesSupplier() { |  | ||||||
|         return this.allQuizzesSupplier; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -98,12 +98,24 @@ final class OpenEdxLmsAPITemplate implements LmsAPITemplate { | ||||||
|                 .collect(Collectors.toList()); |                 .collect(Collectors.toList()); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     @Override | ||||||
|  |     public Result<QuizData> getQuizFromCache(final String id) { | ||||||
|  |         return this.openEdxCourseAccess | ||||||
|  |                 .getQuizFromCache(id) | ||||||
|  |                 .orElse(() -> getQuiz(id)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     @Override |     @Override | ||||||
|     public Collection<Result<QuizData>> getQuizzesFromCache(final Set<String> ids) { |     public Collection<Result<QuizData>> getQuizzesFromCache(final Set<String> ids) { | ||||||
|         return this.openEdxCourseAccess.getQuizzesFromCache(ids) |         return this.openEdxCourseAccess.getQuizzesFromCache(ids) | ||||||
|                 .getOrElse(() -> getQuizzes(ids)); |                 .getOrElse(() -> getQuizzes(ids)); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     @Override | ||||||
|  |     public void clearCache() { | ||||||
|  |         this.openEdxCourseAccess.clearCache(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     @Override |     @Override | ||||||
|     public Result<Chapters> getCourseChapters(final String courseId) { |     public Result<Chapters> getCourseChapters(final String courseId) { | ||||||
|         return Result.tryCatch(() -> this.openEdxCourseAccess |         return Result.tryCatch(() -> this.openEdxCourseAccess | ||||||
|  |  | ||||||
|  | @ -10,6 +10,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.edx; | ||||||
| 
 | 
 | ||||||
| import org.apache.commons.lang3.StringUtils; | import org.apache.commons.lang3.StringUtils; | ||||||
| import org.springframework.beans.factory.annotation.Value; | import org.springframework.beans.factory.annotation.Value; | ||||||
|  | import org.springframework.cache.CacheManager; | ||||||
| import org.springframework.context.annotation.Lazy; | import org.springframework.context.annotation.Lazy; | ||||||
| import org.springframework.core.env.Environment; | import org.springframework.core.env.Environment; | ||||||
| import org.springframework.stereotype.Service; | import org.springframework.stereotype.Service; | ||||||
|  | @ -36,6 +37,7 @@ public class OpenEdxLmsAPITemplateFactory implements LmsAPITemplateFactory { | ||||||
|     private final WebserviceInfo webserviceInfo; |     private final WebserviceInfo webserviceInfo; | ||||||
|     private final AsyncService asyncService; |     private final AsyncService asyncService; | ||||||
|     private final Environment environment; |     private final Environment environment; | ||||||
|  |     private final CacheManager cacheManager; | ||||||
|     private final ClientCredentialService clientCredentialService; |     private final ClientCredentialService clientCredentialService; | ||||||
|     private final ClientHttpRequestFactoryService clientHttpRequestFactoryService; |     private final ClientHttpRequestFactoryService clientHttpRequestFactoryService; | ||||||
|     private final String[] alternativeTokenRequestPaths; |     private final String[] alternativeTokenRequestPaths; | ||||||
|  | @ -46,6 +48,7 @@ public class OpenEdxLmsAPITemplateFactory implements LmsAPITemplateFactory { | ||||||
|             final WebserviceInfo webserviceInfo, |             final WebserviceInfo webserviceInfo, | ||||||
|             final AsyncService asyncService, |             final AsyncService asyncService, | ||||||
|             final Environment environment, |             final Environment environment, | ||||||
|  |             final CacheManager cacheManager, | ||||||
|             final ClientCredentialService clientCredentialService, |             final ClientCredentialService clientCredentialService, | ||||||
|             final ClientHttpRequestFactoryService clientHttpRequestFactoryService, |             final ClientHttpRequestFactoryService clientHttpRequestFactoryService, | ||||||
|             @Value("${sebserver.webservice.lms.openedx.api.token.request.paths}") final String alternativeTokenRequestPaths, |             @Value("${sebserver.webservice.lms.openedx.api.token.request.paths}") final String alternativeTokenRequestPaths, | ||||||
|  | @ -55,6 +58,7 @@ public class OpenEdxLmsAPITemplateFactory implements LmsAPITemplateFactory { | ||||||
|         this.webserviceInfo = webserviceInfo; |         this.webserviceInfo = webserviceInfo; | ||||||
|         this.asyncService = asyncService; |         this.asyncService = asyncService; | ||||||
|         this.environment = environment; |         this.environment = environment; | ||||||
|  |         this.cacheManager = cacheManager; | ||||||
|         this.clientCredentialService = clientCredentialService; |         this.clientCredentialService = clientCredentialService; | ||||||
|         this.clientHttpRequestFactoryService = clientHttpRequestFactoryService; |         this.clientHttpRequestFactoryService = clientHttpRequestFactoryService; | ||||||
|         this.alternativeTokenRequestPaths = (alternativeTokenRequestPaths != null) |         this.alternativeTokenRequestPaths = (alternativeTokenRequestPaths != null) | ||||||
|  | @ -84,7 +88,8 @@ public class OpenEdxLmsAPITemplateFactory implements LmsAPITemplateFactory { | ||||||
|                     openEdxRestTemplateFactory, |                     openEdxRestTemplateFactory, | ||||||
|                     this.webserviceInfo, |                     this.webserviceInfo, | ||||||
|                     this.asyncService, |                     this.asyncService, | ||||||
|                     this.environment); |                     this.environment, | ||||||
|  |                     this.cacheManager); | ||||||
| 
 | 
 | ||||||
|             final OpenEdxCourseRestriction openEdxCourseRestriction = new OpenEdxCourseRestriction( |             final OpenEdxCourseRestriction openEdxCourseRestriction = new OpenEdxCourseRestriction( | ||||||
|                     this.jsonMapper, |                     this.jsonMapper, | ||||||
|  |  | ||||||
|  | @ -9,7 +9,9 @@ | ||||||
| package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.mockup; | package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.mockup; | ||||||
| 
 | 
 | ||||||
| import java.util.ArrayList; | import java.util.ArrayList; | ||||||
|  | import java.util.Arrays; | ||||||
| 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; | ||||||
| import java.util.stream.Collectors; | import java.util.stream.Collectors; | ||||||
|  | @ -181,6 +183,16 @@ public class MockupLmsAPITemplate implements LmsAPITemplate { | ||||||
|         return getQuizzes(ids); |         return getQuizzes(ids); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     @Override | ||||||
|  |     public Result<QuizData> getQuizFromCache(final String id) { | ||||||
|  |         return getQuizzes(new HashSet<>(Arrays.asList(id))).iterator().next(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void clearCache() { | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     @Override |     @Override | ||||||
|     public Result<Chapters> getCourseChapters(final String courseId) { |     public Result<Chapters> getCourseChapters(final String courseId) { | ||||||
|         return Result.ofError(new UnsupportedOperationException()); |         return Result.ofError(new UnsupportedOperationException()); | ||||||
|  |  | ||||||
|  | @ -14,6 +14,7 @@ import java.util.Collections; | ||||||
| import java.util.HashMap; | import java.util.HashMap; | ||||||
| import java.util.List; | import java.util.List; | ||||||
| import java.util.Map; | import java.util.Map; | ||||||
|  | import java.util.NoSuchElementException; | ||||||
| import java.util.Set; | import java.util.Set; | ||||||
| import java.util.function.Function; | import java.util.function.Function; | ||||||
| import java.util.function.Supplier; | import java.util.function.Supplier; | ||||||
|  | @ -37,6 +38,7 @@ import ch.ethz.seb.sebserver.gbl.Constants; | ||||||
| import ch.ethz.seb.sebserver.gbl.api.JSONMapper; | import ch.ethz.seb.sebserver.gbl.api.JSONMapper; | ||||||
| import ch.ethz.seb.sebserver.gbl.async.AsyncService; | import ch.ethz.seb.sebserver.gbl.async.AsyncService; | ||||||
| import ch.ethz.seb.sebserver.gbl.async.CircuitBreaker; | import ch.ethz.seb.sebserver.gbl.async.CircuitBreaker; | ||||||
|  | import ch.ethz.seb.sebserver.gbl.async.CircuitBreaker.State; | ||||||
| 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; | ||||||
|  | @ -48,6 +50,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier; | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.AbstractCourseAccess; | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.AbstractCourseAccess; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleCourseDataAsyncLoader.CourseDataShort; | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleCourseDataAsyncLoader.CourseDataShort; | ||||||
|  | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleCourseDataAsyncLoader.CourseQuizShort; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory.MoodleAPIRestTemplate; | import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory.MoodleAPIRestTemplate; | ||||||
| 
 | 
 | ||||||
| /** Implements the LmsAPITemplate for Open edX LMS Course API access. | /** Implements the LmsAPITemplate for Open edX LMS Course API access. | ||||||
|  | @ -90,7 +93,6 @@ public class MoodleCourseAccess extends AbstractCourseAccess { | ||||||
|     private final MoodleRestTemplateFactory moodleRestTemplateFactory; |     private final MoodleRestTemplateFactory moodleRestTemplateFactory; | ||||||
|     private final MoodleCourseDataAsyncLoader moodleCourseDataAsyncLoader; |     private final MoodleCourseDataAsyncLoader moodleCourseDataAsyncLoader; | ||||||
|     private final CircuitBreaker<List<QuizData>> allQuizzesRequest; |     private final CircuitBreaker<List<QuizData>> allQuizzesRequest; | ||||||
|     private final AllQuizzesSupplier allQuizzesSupplier; |  | ||||||
| 
 | 
 | ||||||
|     private MoodleAPIRestTemplate restTemplate; |     private MoodleAPIRestTemplate restTemplate; | ||||||
| 
 | 
 | ||||||
|  | @ -119,19 +121,6 @@ public class MoodleCourseAccess extends AbstractCourseAccess { | ||||||
|                         "sebserver.webservice.circuitbreaker.allQuizzesRequest.timeToRecover", |                         "sebserver.webservice.circuitbreaker.allQuizzesRequest.timeToRecover", | ||||||
|                         Long.class, |                         Long.class, | ||||||
|                         Constants.MINUTE_IN_MILLIS)); |                         Constants.MINUTE_IN_MILLIS)); | ||||||
| 
 |  | ||||||
|         this.allQuizzesSupplier = new AllQuizzesSupplier() { |  | ||||||
| 
 |  | ||||||
|             @Override |  | ||||||
|             public List<QuizData> getAllCached() { |  | ||||||
|                 return getCached(); |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             @Override |  | ||||||
|             public Result<List<QuizData>> getAll(final FilterMap filterMap) { |  | ||||||
|                 return MoodleCourseAccess.this.allQuizzesRequest.protectedRun(allQuizzesSupplier(filterMap)); |  | ||||||
|             } |  | ||||||
|         }; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     APITemplateDataSupplier getApiTemplateDataSupplier() { |     APITemplateDataSupplier getApiTemplateDataSupplier() { | ||||||
|  | @ -223,6 +212,77 @@ public class MoodleCourseAccess extends AbstractCourseAccess { | ||||||
|         return LmsSetupTestResult.ofOkay(); |         return LmsSetupTestResult.ofOkay(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     @Override | ||||||
|  |     public Result<QuizData> getQuizFromCache(final String id) { | ||||||
|  |         return Result.tryCatch(() -> { | ||||||
|  | 
 | ||||||
|  |             final Map<String, CourseDataShort> cachedCourseData = this.moodleCourseDataAsyncLoader | ||||||
|  |                     .getCachedCourseData(); | ||||||
|  | 
 | ||||||
|  |             final String courseId = getCourseId(id); | ||||||
|  |             final String quizId = getQuizId(id); | ||||||
|  |             if (cachedCourseData.containsKey(courseId)) { | ||||||
|  |                 final CourseDataShort courseData = cachedCourseData.get(courseId); | ||||||
|  |                 final CourseQuizShort quiz = courseData.quizzes | ||||||
|  |                         .stream() | ||||||
|  |                         .filter(q -> q.id.equals(quizId)) | ||||||
|  |                         .findFirst() | ||||||
|  |                         .orElse(null); | ||||||
|  | 
 | ||||||
|  |                 if (quiz != null) { | ||||||
|  |                     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); | ||||||
|  |                     final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup(); | ||||||
|  |                     final String urlPrefix = (lmsSetup.lmsApiUrl.endsWith(Constants.URL_PATH_SEPARATOR)) | ||||||
|  |                             ? lmsSetup.lmsApiUrl + MOODLE_QUIZ_START_URL_PATH | ||||||
|  |                             : lmsSetup.lmsApiUrl + Constants.URL_PATH_SEPARATOR + MOODLE_QUIZ_START_URL_PATH; | ||||||
|  | 
 | ||||||
|  |                     return createQuizData(lmsSetup, courseData, urlPrefix, additionalAttrs, quiz); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             throw new RuntimeException("No quiz found in cache"); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public Result<Collection<Result<QuizData>>> getQuizzesFromCache(final Set<String> ids) { | ||||||
|  |         return Result.tryCatch(() -> { | ||||||
|  |             final List<QuizData> cached = getCached(); | ||||||
|  |             final List<QuizData> available = (cached != null) | ||||||
|  |                     ? cached | ||||||
|  |                     : Collections.emptyList(); | ||||||
|  | 
 | ||||||
|  |             final Map<String, QuizData> quizMapping = available | ||||||
|  |                     .stream() | ||||||
|  |                     .collect(Collectors.toMap(q -> q.id, Function.identity())); | ||||||
|  | 
 | ||||||
|  |             if (!quizMapping.keySet().containsAll(ids)) { | ||||||
|  | 
 | ||||||
|  |                 final Map<String, QuizData> collect = quizzesSupplier(ids).get() | ||||||
|  |                         .stream() | ||||||
|  |                         .collect(Collectors.toMap(qd -> qd.id, Function.identity())); | ||||||
|  |                 if (collect != null) { | ||||||
|  |                     quizMapping.clear(); | ||||||
|  |                     quizMapping.putAll(collect); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             return ids | ||||||
|  |                     .stream() | ||||||
|  |                     .map(id -> { | ||||||
|  |                         final QuizData q = quizMapping.get(id); | ||||||
|  |                         return (q == null) | ||||||
|  |                                 ? Result.<QuizData> ofError(new NoSuchElementException("Quiz with id: " + id)) | ||||||
|  |                                 : Result.of(q); | ||||||
|  |                     }) | ||||||
|  |                     .collect(Collectors.toList()); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     @Override |     @Override | ||||||
|     protected Supplier<List<QuizData>> quizzesSupplier(final Set<String> ids) { |     protected Supplier<List<QuizData>> quizzesSupplier(final Set<String> ids) { | ||||||
|         return () -> getRestTemplate() |         return () -> getRestTemplate() | ||||||
|  | @ -231,6 +291,7 @@ public class MoodleCourseAccess extends AbstractCourseAccess { | ||||||
| 
 | 
 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     @Override | ||||||
|     protected Supplier<List<QuizData>> allQuizzesSupplier(final FilterMap filterMap) { |     protected Supplier<List<QuizData>> allQuizzesSupplier(final FilterMap filterMap) { | ||||||
|         return () -> getRestTemplate() |         return () -> getRestTemplate() | ||||||
|                 .map(template -> collectAllQuizzes(template, filterMap)) |                 .map(template -> collectAllQuizzes(template, filterMap)) | ||||||
|  | @ -244,6 +305,9 @@ public class MoodleCourseAccess extends AbstractCourseAccess { | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     protected FetchStatus getFetchStatus() { |     protected FetchStatus getFetchStatus() { | ||||||
|  |         if (this.allQuizzesRequest.getState() != State.CLOSED) { | ||||||
|  |             return FetchStatus.FETCH_ERROR; | ||||||
|  |         } | ||||||
|         if (this.moodleCourseDataAsyncLoader.isRunning()) { |         if (this.moodleCourseDataAsyncLoader.isRunning()) { | ||||||
|             return FetchStatus.ASYNC_FETCH_RUNNING; |             return FetchStatus.ASYNC_FETCH_RUNNING; | ||||||
|         } |         } | ||||||
|  | @ -251,9 +315,8 @@ public class MoodleCourseAccess extends AbstractCourseAccess { | ||||||
|         return FetchStatus.ALL_FETCHED; |         return FetchStatus.ALL_FETCHED; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     public void clearCache() { | ||||||
|     protected AllQuizzesSupplier allQuizzesSupplier() { |         this.moodleCourseDataAsyncLoader.clearCache(); | ||||||
|         return this.allQuizzesSupplier; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private List<QuizData> collectAllQuizzes( |     private List<QuizData> collectAllQuizzes( | ||||||
|  | @ -261,7 +324,6 @@ public class MoodleCourseAccess extends AbstractCourseAccess { | ||||||
|             final FilterMap filterMap) { |             final FilterMap filterMap) { | ||||||
| 
 | 
 | ||||||
|         final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup(); |         final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup(); | ||||||
| 
 |  | ||||||
|         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; | ||||||
|  | @ -272,7 +334,7 @@ public class MoodleCourseAccess extends AbstractCourseAccess { | ||||||
|         // Verify and call the proper strategy to get the course and quiz data |         // Verify and call the proper strategy to get the course and quiz data | ||||||
|         Collection<CourseDataShort> courseQuizData = Collections.emptyList(); |         Collection<CourseDataShort> courseQuizData = Collections.emptyList(); | ||||||
|         if (this.moodleCourseDataAsyncLoader.isRunning()) { |         if (this.moodleCourseDataAsyncLoader.isRunning()) { | ||||||
|             courseQuizData = this.moodleCourseDataAsyncLoader.getCachedCourseData(); |             courseQuizData = this.moodleCourseDataAsyncLoader.getCachedCourseData().values(); | ||||||
|         } else if (this.moodleCourseDataAsyncLoader.getLastRunTime() <= 0) { |         } else if (this.moodleCourseDataAsyncLoader.getLastRunTime() <= 0) { | ||||||
|             // set cut time if available |             // set cut time if available | ||||||
|             if (fromCutTime >= 0) { |             if (fromCutTime >= 0) { | ||||||
|  | @ -282,7 +344,7 @@ public class MoodleCourseAccess extends AbstractCourseAccess { | ||||||
|             this.moodleCourseDataAsyncLoader.loadAsync(restTemplate); |             this.moodleCourseDataAsyncLoader.loadAsync(restTemplate); | ||||||
|             try { |             try { | ||||||
|                 Thread.sleep(INITIAL_WAIT_TIME); |                 Thread.sleep(INITIAL_WAIT_TIME); | ||||||
|                 courseQuizData = this.moodleCourseDataAsyncLoader.getCachedCourseData(); |                 courseQuizData = this.moodleCourseDataAsyncLoader.getCachedCourseData().values(); | ||||||
|             } catch (final Exception e) { |             } catch (final Exception e) { | ||||||
|                 log.error("Failed to wait for first load run: ", e); |                 log.error("Failed to wait for first load run: ", e); | ||||||
|                 return Collections.emptyList(); |                 return Collections.emptyList(); | ||||||
|  | @ -290,7 +352,7 @@ public class MoodleCourseAccess extends AbstractCourseAccess { | ||||||
|         } else if (this.moodleCourseDataAsyncLoader.isLongRunningTask()) { |         } else if (this.moodleCourseDataAsyncLoader.isLongRunningTask()) { | ||||||
|             // on long running tasks if we have a different fromCutTime as before |             // on long running tasks if we have a different fromCutTime as before | ||||||
|             // kick off the lazy loading task immediately with the new time filter |             // kick off the lazy loading task immediately with the new time filter | ||||||
|             courseQuizData = this.moodleCourseDataAsyncLoader.getCachedCourseData(); |             courseQuizData = this.moodleCourseDataAsyncLoader.getCachedCourseData().values(); | ||||||
|             if (fromCutTime > 0 && fromCutTime != this.moodleCourseDataAsyncLoader.getFromCutTime()) { |             if (fromCutTime > 0 && fromCutTime != this.moodleCourseDataAsyncLoader.getFromCutTime()) { | ||||||
|                 this.moodleCourseDataAsyncLoader.setFromCutTime(fromCutTime); |                 this.moodleCourseDataAsyncLoader.setFromCutTime(fromCutTime); | ||||||
|                 this.moodleCourseDataAsyncLoader.loadAsync(restTemplate); |                 this.moodleCourseDataAsyncLoader.loadAsync(restTemplate); | ||||||
|  | @ -306,7 +368,7 @@ public class MoodleCourseAccess extends AbstractCourseAccess { | ||||||
|                 this.moodleCourseDataAsyncLoader.setFromCutTime(fromCutTime); |                 this.moodleCourseDataAsyncLoader.setFromCutTime(fromCutTime); | ||||||
|             } |             } | ||||||
|             this.moodleCourseDataAsyncLoader.loadSync(restTemplate); |             this.moodleCourseDataAsyncLoader.loadSync(restTemplate); | ||||||
|             courseQuizData = this.moodleCourseDataAsyncLoader.getCachedCourseData(); |             courseQuizData = this.moodleCourseDataAsyncLoader.getCachedCourseData().values(); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (courseQuizData.isEmpty()) { |         if (courseQuizData.isEmpty()) { | ||||||
|  | @ -317,7 +379,8 @@ public class MoodleCourseAccess extends AbstractCourseAccess { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private List<QuizData> getCached() { |     private List<QuizData> getCached() { | ||||||
|         final Collection<CourseDataShort> courseQuizData = this.moodleCourseDataAsyncLoader.getCachedCourseData(); |         final Collection<CourseDataShort> courseQuizData = | ||||||
|  |                 this.moodleCourseDataAsyncLoader.getCachedCourseData().values(); | ||||||
|         final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup(); |         final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup(); | ||||||
|         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 | ||||||
|  | @ -534,34 +597,41 @@ public class MoodleCourseAccess extends AbstractCourseAccess { | ||||||
| 
 | 
 | ||||||
|         final List<QuizData> courseAndQuiz = courseData.quizzes |         final List<QuizData> courseAndQuiz = courseData.quizzes | ||||||
|                 .stream() |                 .stream() | ||||||
|                 .map(courseQuizData -> { |                 .map(courseQuizData -> createQuizData(lmsSetup, courseData, uriPrefix, additionalAttrs, 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(), |  | ||||||
|                             courseQuizData.name, |  | ||||||
|                             Constants.EMPTY_NOTE, |  | ||||||
|                             (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()); |                 .collect(Collectors.toList()); | ||||||
| 
 | 
 | ||||||
|         return courseAndQuiz; |         return courseAndQuiz; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     private QuizData createQuizData( | ||||||
|  |             final LmsSetup lmsSetup, | ||||||
|  |             final CourseDataShort courseData, | ||||||
|  |             final String uriPrefix, | ||||||
|  |             final Map<String, String> additionalAttrs, | ||||||
|  |             final CourseQuizShort courseQuizData) { | ||||||
|  | 
 | ||||||
|  |         final String startURI = uriPrefix + courseQuizData.course_module; | ||||||
|  |         return new QuizData( | ||||||
|  |                 getInternalQuizId( | ||||||
|  |                         courseQuizData.course_module, | ||||||
|  |                         courseData.id, | ||||||
|  |                         courseData.short_name, | ||||||
|  |                         courseData.idnumber), | ||||||
|  |                 lmsSetup.getInstitutionId(), | ||||||
|  |                 lmsSetup.id, | ||||||
|  |                 lmsSetup.getLmsType(), | ||||||
|  |                 courseQuizData.name, | ||||||
|  |                 Constants.EMPTY_NOTE, | ||||||
|  |                 (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); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     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.moodleRestTemplateFactory | ||||||
|  |  | ||||||
|  | @ -65,7 +65,7 @@ public class MoodleCourseDataAsyncLoader { | ||||||
|     private final int maxSize; |     private final int maxSize; | ||||||
|     private final int pageSize; |     private final int pageSize; | ||||||
| 
 | 
 | ||||||
|     private final Set<CourseDataShort> cachedCourseData = new HashSet<>(); |     private final Map<String, CourseDataShort> cachedCourseData = new HashMap<>(); | ||||||
|     private final Set<String> newIds = new HashSet<>(); |     private final Set<String> newIds = new HashSet<>(); | ||||||
| 
 | 
 | ||||||
|     private String lmsSetup = Constants.EMPTY_NOTE; |     private String lmsSetup = Constants.EMPTY_NOTE; | ||||||
|  | @ -122,8 +122,8 @@ public class MoodleCourseDataAsyncLoader { | ||||||
|         this.fromCutTime = fromCutTime; |         this.fromCutTime = fromCutTime; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public Set<CourseDataShort> getCachedCourseData() { |     public Map<String, CourseDataShort> getCachedCourseData() { | ||||||
|         return new HashSet<>(this.cachedCourseData); |         return new HashMap<>(this.cachedCourseData); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public long getLastRunTime() { |     public long getLastRunTime() { | ||||||
|  | @ -138,7 +138,7 @@ public class MoodleCourseDataAsyncLoader { | ||||||
|         return this.lastLoadTime > 3 * Constants.SECOND_IN_MILLIS; |         return this.lastLoadTime > 3 * Constants.SECOND_IN_MILLIS; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public Set<CourseDataShort> loadSync(final MoodleAPIRestTemplate restTemplate) { |     public Map<String, CourseDataShort> loadSync(final MoodleAPIRestTemplate restTemplate) { | ||||||
|         if (this.running) { |         if (this.running) { | ||||||
|             throw new IllegalStateException("Is already running asynchronously"); |             throw new IllegalStateException("Is already running asynchronously"); | ||||||
|         } |         } | ||||||
|  | @ -168,14 +168,20 @@ public class MoodleCourseDataAsyncLoader { | ||||||
| 
 | 
 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public void clearCache() { | ||||||
|  |         if (!isRunning()) { | ||||||
|  |             this.cachedCourseData.clear(); | ||||||
|  |             this.newIds.clear(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     private Runnable loadAndCache(final MoodleAPIRestTemplate restTemplate) { |     private Runnable loadAndCache(final MoodleAPIRestTemplate restTemplate) { | ||||||
|         return () -> { |         return () -> { | ||||||
|             //this.cachedCourseData.clear(); |  | ||||||
|             this.newIds.clear(); |             this.newIds.clear(); | ||||||
|             final long startTime = Utils.getMillisecondsNow(); |             final long startTime = Utils.getMillisecondsNow(); | ||||||
| 
 | 
 | ||||||
|             loadAllQuizzes(restTemplate); |             loadAllQuizzes(restTemplate); | ||||||
|             syncCache(); |             this.syncCache(); | ||||||
| 
 | 
 | ||||||
|             this.lastLoadTime = Utils.getMillisecondsNow() - startTime; |             this.lastLoadTime = Utils.getMillisecondsNow() - startTime; | ||||||
|             this.running = false; |             this.running = false; | ||||||
|  | @ -263,7 +269,7 @@ public class MoodleCourseDataAsyncLoader { | ||||||
|                                         this.lmsSetup, |                                         this.lmsSetup, | ||||||
|                                         c); |                                         c); | ||||||
|                             } else { |                             } else { | ||||||
|                                 this.cachedCourseData.add(c); |                                 this.cachedCourseData.put(c.id, c); | ||||||
|                                 this.newIds.add(c.id); |                                 this.newIds.add(c.id); | ||||||
|                             } |                             } | ||||||
|                         }); |                         }); | ||||||
|  | @ -461,13 +467,15 @@ public class MoodleCourseDataAsyncLoader { | ||||||
| 
 | 
 | ||||||
|     private void syncCache() { |     private void syncCache() { | ||||||
|         if (!this.cachedCourseData.isEmpty()) { |         if (!this.cachedCourseData.isEmpty()) { | ||||||
|             final Set<CourseDataShort> newData = this.cachedCourseData.stream() | 
 | ||||||
|                     .filter(data -> this.newIds.contains(data.id)) |             final Set<String> oldData = this.cachedCourseData | ||||||
|  |                     .keySet() | ||||||
|  |                     .stream() | ||||||
|  |                     .filter(id -> !this.newIds.contains(id)) | ||||||
|                     .collect(Collectors.toSet()); |                     .collect(Collectors.toSet()); | ||||||
| 
 | 
 | ||||||
|             synchronized (this.cachedCourseData) { |             synchronized (this.cachedCourseData) { | ||||||
|                 this.cachedCourseData.clear(); |                 oldData.stream().forEach(this.cachedCourseData::remove); | ||||||
|                 this.cachedCourseData.addAll(newData); |  | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         this.newIds.clear(); |         this.newIds.clear(); | ||||||
|  |  | ||||||
|  | @ -110,6 +110,17 @@ public class MoodleLmsAPITemplate implements LmsAPITemplate { | ||||||
|                 .collect(Collectors.toList()); |                 .collect(Collectors.toList()); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     @Override | ||||||
|  |     public Result<QuizData> getQuizFromCache(final String id) { | ||||||
|  |         return this.moodleCourseAccess.getQuizFromCache(id) | ||||||
|  |                 .orElse(() -> getQuiz(id)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void clearCache() { | ||||||
|  |         this.moodleCourseAccess.clearCache(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     @Override |     @Override | ||||||
|     public Collection<Result<QuizData>> getQuizzesFromCache(final Set<String> ids) { |     public Collection<Result<QuizData>> getQuizzesFromCache(final Set<String> ids) { | ||||||
|         return this.moodleCourseAccess.getQuizzesFromCache(ids) |         return this.moodleCourseAccess.getQuizzesFromCache(ids) | ||||||
|  |  | ||||||
|  | @ -123,7 +123,8 @@ public class ExamSessionServiceImpl implements ExamSessionService { | ||||||
|         return Result.tryCatch(() -> { |         return Result.tryCatch(() -> { | ||||||
|             final Collection<APIMessage> result = new ArrayList<>(); |             final Collection<APIMessage> result = new ArrayList<>(); | ||||||
| 
 | 
 | ||||||
|             final Exam exam = this.examDAO.byPK(examId) |             final Exam exam = this.examDAO | ||||||
|  |                     .getWithQuizDataFromCache(examId) | ||||||
|                     .getOrThrow(); |                     .getOrThrow(); | ||||||
| 
 | 
 | ||||||
|             // check lms connection |             // check lms connection | ||||||
|  |  | ||||||
|  | @ -279,7 +279,8 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> { | ||||||
|                     defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId) { |                     defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId) { | ||||||
| 
 | 
 | ||||||
|         checkReadPrivilege(institutionId); |         checkReadPrivilege(institutionId); | ||||||
|         return this.examDAO.byPK(modelId) |         return this.examDAO | ||||||
|  |                 .getWithQuizDataFromCache(modelId) | ||||||
|                 .flatMap(this.examAdminService::isRestricted) |                 .flatMap(this.examAdminService::isRestricted) | ||||||
|                 .getOrThrow(); |                 .getOrThrow(); | ||||||
|     } |     } | ||||||
|  | @ -407,9 +408,7 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> { | ||||||
|             @PathVariable final Long modelId) { |             @PathVariable final Long modelId) { | ||||||
| 
 | 
 | ||||||
|         checkReadPrivilege(institutionId); |         checkReadPrivilege(institutionId); | ||||||
|         return this.entityDAO.byPK(modelId) |         return this.examAdminService.getProctoringServiceSettings(modelId) | ||||||
|                 .flatMap(this.authorization::checkRead) |  | ||||||
|                 .flatMap(exam -> this.examAdminService.getProctoringServiceSettings(exam.id)) |  | ||||||
|                 .getOrThrow(); |                 .getOrThrow(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -23,7 +23,7 @@ | ||||||
|             <tti unit="hours">24</tti> |             <tti unit="hours">24</tti> | ||||||
|         </expiry> |         </expiry> | ||||||
|         <resources> |         <resources> | ||||||
|             <heap unit="entries">2000</heap> |             <heap unit="entries">3000</heap> | ||||||
|         </resources> |         </resources> | ||||||
|     </cache> |     </cache> | ||||||
|      |      | ||||||
|  | @ -93,7 +93,16 @@ | ||||||
|         </resources> |         </resources> | ||||||
|     </cache> |     </cache> | ||||||
|      |      | ||||||
|      |     <cache alias="QUIZ_DATA_CACHE"> | ||||||
|  |         <key-type>java.lang.String</key-type> | ||||||
|  |         <value-type>ch.ethz.seb.sebserver.gbl.model.exam.QuizData</value-type> | ||||||
|  |         <expiry> | ||||||
|  |             <tti unit="minutes">10</tti> | ||||||
|  |         </expiry> | ||||||
|  |         <resources> | ||||||
|  |             <heap unit="entries">10000</heap> | ||||||
|  |         </resources> | ||||||
|  |     </cache> | ||||||
|      |      | ||||||
|      |      | ||||||
| </config> | </config> | ||||||
		Loading…
	
	Add table
		
		Reference in a new issue
	
	 anhefti
						anhefti