diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/Constants.java b/src/main/java/ch/ethz/seb/sebserver/gbl/Constants.java index 059fda0d..2ab1481f 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/Constants.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/Constants.java @@ -59,6 +59,7 @@ public final class Constants { public static final Character DOUBLE_QUOTE = '"'; public static final Character COMMA = ','; public static final Character PIPE = '|'; + public static final Character UNDERLINE = '_'; public static final Character AMPERSAND = '&'; public static final Character EQUALITY_SIGN = '='; public static final Character LIST_SEPARATOR_CHAR = COMMA; diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ExamDAO.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ExamDAO.java index f651b48f..4b76abec 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ExamDAO.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ExamDAO.java @@ -37,6 +37,8 @@ public interface ExamDAO extends ActivatableEntityDAO, BulkActionSup * happened */ Result examGrantEntityByClientConnection(Long connectionId); + Result getWithQuizDataFromCache(Long id); + /** Get all active Exams for a given institution. * * @param institutionId the identifier of the institution diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamConfigurationMapDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamConfigurationMapDAOImpl.java index 5391b966..59a919fd 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamConfigurationMapDAOImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamConfigurationMapDAOImpl.java @@ -377,6 +377,7 @@ public class ExamConfigurationMapDAOImpl implements ExamConfigurationMapDAO { private Result> toDomainModel( final Collection records) { + return Result.tryCatch(() -> records .stream() .map(model -> this.toDomainModel(model).getOrThrow()) @@ -390,7 +391,8 @@ public class ExamConfigurationMapDAOImpl implements ExamConfigurationMapDAO { .selectByPrimaryKey(record.getConfigurationNodeId()); final String status = config.getStatus(); - final Exam exam = this.examDAO.byPK(record.getExamId()) + final Exam exam = this.examDAO + .getWithQuizDataFromCache(record.getExamId()) .getOr(null); return new ExamConfigurationMap( diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamDAOImpl.java index 99ce6240..1e6920c5 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamDAOImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamDAOImpl.java @@ -119,6 +119,13 @@ public class ExamDAOImpl implements ExamDAO { .map(record -> toDomainModel(record, null, null).getOrThrow()); } + @Override + @Transactional(readOnly = true) + public Result getWithQuizDataFromCache(final Long id) { + return recordById(id) + .flatMap(this::toDomainModelFromCache); + } + @Override @Transactional(readOnly = true) public Result> all(final Long institutionId, final Boolean active) { @@ -761,6 +768,17 @@ public class ExamDAOImpl implements ExamDAO { exam.getDescription()); } + private Result toDomainModelFromCache(final ExamRecord record) { + + return this.lmsAPIService + .getLmsAPITemplate(record.getLmsSetupId()) + .flatMap(template -> this.toDomainModel( + record, + template.getQuizFromCache(record.getExternalId()) + .getOrThrow(), + null)); + } + private Result toDomainModel(final ExamRecord record) { return toDomainModel( record.getLmsSetupId(), diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/LmsAPITemplate.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/LmsAPITemplate.java index aec35ad1..2a40847e 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/LmsAPITemplate.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/LmsAPITemplate.java @@ -165,6 +165,15 @@ public interface LmsAPITemplate { * @return Collection of all {@link QuizData } from the given id set */ Collection> getQuizzesFromCache(Set 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 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, * to LMS examinee account details by requesting them on the LMS API with the given examineeUserId * diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/AbstractCachedCourseAccess.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/AbstractCachedCourseAccess.java new file mode 100644 index 00000000..15473d59 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/AbstractCachedCourseAccess.java @@ -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.stream().forEach(q -> this.cache.put(createCacheKey(q.id), q)); + } + + protected void evict(final String id) { + this.cache.evict(createCacheKey(id)); + } + + @Override + public Result 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>> getQuizzesFromCache(final Set 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(); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/AbstractCourseAccess.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/AbstractCourseAccess.java index b43fb874..0b0afa2c 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/AbstractCourseAccess.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/AbstractCourseAccess.java @@ -11,12 +11,8 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl; import java.util.Collection; import java.util.Collections; import java.util.List; -import java.util.Map; -import java.util.NoSuchElementException; import java.util.Set; -import java.util.function.Function; import java.util.function.Supplier; -import java.util.stream.Collectors; import org.slf4j.Logger; 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.async.AsyncService; 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.QuizData; import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails; @@ -101,42 +98,8 @@ public abstract class AbstractCourseAccess { Constants.SECOND_IN_MILLIS * 10)); } - public Result>> getQuizzesFromCache(final Set ids) { - return Result.tryCatch(() -> { - final List cached = allQuizzesSupplier().getAllCached(); - final List available = (cached != null) - ? cached - : Collections.emptyList(); - - final Map quizMapping = available - .stream() - .collect(Collectors.toMap(q -> q.id, Function.identity())); - - if (!quizMapping.keySet().containsAll(ids)) { - - final Map 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. ofError(new NoSuchElementException("Quiz with id: " + id)) - : Result.of(q); - }) - .collect(Collectors.toList()); - }); - } - public Result> getQuizzes(final FilterMap filterMap) { - return allQuizzesSupplier().getAll(filterMap); + return this.quizzesRequest.protectedRun(allQuizzesSupplier(filterMap)); } public Result getExamineeAccountDetails(final String examineeSessionId) { @@ -172,30 +135,41 @@ public abstract class AbstractCourseAccess { Collections.emptyMap()); } + /** This abstraction has no cache implementation and therefore this returns a Result + * with an "No cache supported error. + *

+ * 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 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. + *

+ * 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>> getQuizzesFromCache(final Set ids) { + return Result.ofRuntimeError("No cache supported"); + } + /** Provides a supplier for the quiz data request to use within the circuit breaker */ protected abstract Supplier> quizzesSupplier(final Set ids); - /** Provides a AllQuizzesSupplier to supply quiz data either form cache or from LMS */ - protected abstract AllQuizzesSupplier allQuizzesSupplier(); + /** Provides a supplier to supply request to use within the circuit breaker */ + protected abstract Supplier> allQuizzesSupplier(final FilterMap filterMap); /** Provides a supplier for the course chapter data request to use within the circuit breaker */ protected abstract Supplier getCourseChaptersSupplier(final String courseId); - /** Gives a fetch status if asynchronous quiz data fetching is part of the underling implementation */ - protected abstract FetchStatus getFetchStatus(); - - /** Uses to supply quiz data */ - protected interface AllQuizzesSupplier { - /** Get all currently cached quiz data if supported by the underling implementation - * - * @return List containing all cached quiz data objects */ - List 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> getAll(final FilterMap filterMap); + protected FetchStatus getFetchStatus() { + if (this.quizzesRequest.getState() != State.CLOSED) { + return FetchStatus.FETCH_ERROR; + } + return FetchStatus.ALL_FETCHED; } - } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsAPIServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsAPIServiceImpl.java index 407bcd40..05b1e7fb 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsAPIServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsAPIServiceImpl.java @@ -98,17 +98,20 @@ public class LmsAPIServiceImpl implements LmsAPIService { @Override public Result getLmsAPITemplate(final String lmsSetupId) { return Result.tryCatch(() -> { - LmsAPITemplate lmsAPITemplate = getFromCache(lmsSetupId); - if (lmsAPITemplate == null) { - lmsAPITemplate = createLmsSetupTemplate(lmsSetupId); - if (lmsAPITemplate != null) { - this.cache.put(new CacheKey(lmsSetupId, System.currentTimeMillis()), lmsAPITemplate); + synchronized (this) { + LmsAPITemplate lmsAPITemplate = getFromCache(lmsSetupId); + if (lmsAPITemplate == null) { + lmsAPITemplate = createLmsSetupTemplate(lmsSetupId); + if (lmsAPITemplate != null) { + this.cache.put(new CacheKey(lmsSetupId, System.currentTimeMillis()), lmsAPITemplate); + } } + if (lmsAPITemplate == null) { + throw new ResourceNotFoundException(EntityType.LMS_SETUP, lmsSetupId); + } + + return lmsAPITemplate; } - if (lmsAPITemplate == null) { - throw new ResourceNotFoundException(EntityType.LMS_SETUP, lmsSetupId); - } - return lmsAPITemplate; }); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/SEBRestrictionServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/SEBRestrictionServiceImpl.java index 0c2c85a8..081e0ad0 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/SEBRestrictionServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/SEBRestrictionServiceImpl.java @@ -91,12 +91,10 @@ public class SEBRestrictionServiceImpl implements SEBRestrictionService { public Result getSEBRestrictionFromExam(final Exam exam) { return Result.tryCatch(() -> { // load the config keys from restriction and merge with new generated config keys - final long currentTimeMillis = System.currentTimeMillis(); final Set configKeys = new HashSet<>(); final Collection generatedKeys = this.examConfigService .generateConfigKeys(exam.institutionId, exam.id) .getOrThrow(); - System.out.println("******* " + (System.currentTimeMillis() - currentTimeMillis)); configKeys.addAll(generatedKeys); if (generatedKeys != null && !generatedKeys.isEmpty()) { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxCourseAccess.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxCourseAccess.java index 54d36cd3..29f705c5 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxCourseAccess.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxCourseAccess.java @@ -21,6 +21,7 @@ import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.cache.CacheManager; import org.springframework.core.env.Environment; import org.springframework.http.HttpEntity; 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.api.JSONMapper; 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.QuizData; 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.servicelayer.dao.FilterMap; 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. * * 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); @@ -74,65 +73,34 @@ final class OpenEdxCourseAccess extends AbstractCourseAccess { private final JSONMapper jsonMapper; private final OpenEdxRestTemplateFactory openEdxRestTemplateFactory; private final WebserviceInfo webserviceInfo; - private final MemoizingCircuitBreaker> allQuizzesRequest; - private final AllQuizzesSupplier allQuizzesSupplier; private OAuth2RestTemplate restTemplate; + private final Long lmsSetupId; public OpenEdxCourseAccess( final JSONMapper jsonMapper, final OpenEdxRestTemplateFactory openEdxRestTemplateFactory, final WebserviceInfo webserviceInfo, final AsyncService asyncService, - final Environment environment) { + final Environment environment, + final CacheManager cacheManager) { - super(asyncService, environment); + super(asyncService, environment, cacheManager); this.jsonMapper = jsonMapper; this.openEdxRestTemplateFactory = openEdxRestTemplateFactory; this.webserviceInfo = webserviceInfo; - - 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 getAllCached() { - return OpenEdxCourseAccess.this.allQuizzesRequest.getCached(); - } - - @Override - public Result> getAll(final FilterMap filterMap) { - return OpenEdxCourseAccess.this.allQuizzesRequest.get(); - } - - }; + this.lmsSetupId = openEdxRestTemplateFactory.apiTemplateDataSupplier.getLmsSetup().id; } APITemplateDataSupplier getApiTemplateDataSupplier() { return this.openEdxRestTemplateFactory.apiTemplateDataSupplier; } + @Override + protected Long getLmsSetupId() { + return this.lmsSetupId; + } + LmsSetupTestResult initAPIAccess() { final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup(); @@ -219,6 +187,13 @@ final class OpenEdxCourseAccess extends AbstractCourseAccess { }); } + @Override + protected Supplier> allQuizzesSupplier(final FilterMap filterMap) { + return () -> getRestTemplate() + .map(this::collectAllQuizzes) + .getOrThrow(); + } + @Override protected Supplier> quizzesSupplier(final Set ids) { @@ -231,7 +206,7 @@ final class OpenEdxCourseAccess extends AbstractCourseAccess { getOneCourses( lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_ENDPOINT, getRestTemplate().getOrThrow(), - ids.iterator().next()), + ids), externalStartURI)); }; } else { @@ -241,12 +216,6 @@ final class OpenEdxCourseAccess extends AbstractCourseAccess { } } - private Supplier> quizzesSupplier() { - return () -> getRestTemplate() - .map(this::collectAllQuizzes) - .getOrThrow(); - } - @Override protected Supplier getCourseChaptersSupplier(final String courseId) { return () -> { @@ -337,7 +306,10 @@ final class OpenEdxCourseAccess extends AbstractCourseAccess { final List collector = new ArrayList<>(); EdXPage page = getEdxPage(pageURI, restTemplate).getBody(); 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)) { page = getEdxPage(page.next, restTemplate).getBody(); if (page != null) { @@ -355,16 +327,33 @@ final class OpenEdxCourseAccess extends AbstractCourseAccess { private CourseData getOneCourses( final String pageURI, final OAuth2RestTemplate restTemplate, - final String id) { + final Set ids) { - final HttpHeaders httpHeaders = new HttpHeaders(); - final ResponseEntity exchange = restTemplate.exchange( - pageURI + "/" + id, - HttpMethod.GET, - new HttpEntity<>(httpHeaders), - CourseData.class); + System.out.println("********************"); - 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 exchange = restTemplate.exchange( + uri, + HttpMethod.GET, + new HttpEntity<>(httpHeaders), + CourseData.class); + + return exchange.getBody(); + } catch (final Exception e) { + // try with paging + final List collectCourses = collectCourses(pageURI, restTemplate, ids); + if (collectCourses.isEmpty()) { + return null; + } + return collectCourses.get(0); + } } private List collectAllCourses(final String pageURI, final OAuth2RestTemplate restTemplate) { @@ -403,7 +392,7 @@ final class OpenEdxCourseAccess extends AbstractCourseAccess { Blocks.class); } - private static QuizData quizDataOf( + private QuizData quizDataOf( final LmsSetup lmsSetup, final CourseData courseData, final String uriPrefix) { @@ -411,7 +400,7 @@ final class OpenEdxCourseAccess extends AbstractCourseAccess { final String startURI = uriPrefix + courseData.id; final Map additionalAttrs = new HashMap<>(); additionalAttrs.put("blocks_url", courseData.blocks_url); - return new QuizData( + final QuizData quizData = new QuizData( courseData.id, lmsSetup.getInstitutionId(), lmsSetup.id, @@ -421,6 +410,10 @@ final class OpenEdxCourseAccess extends AbstractCourseAccess { courseData.start, courseData.end, startURI); + + super.putToCache(quizData); + + return quizData; } /** Maps a OpenEdX course API course page */ @@ -538,17 +531,4 @@ final class OpenEdxCourseAccess extends AbstractCourseAccess { 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; - } - } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxLmsAPITemplate.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxLmsAPITemplate.java index 47993903..7a1c068e 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxLmsAPITemplate.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxLmsAPITemplate.java @@ -98,12 +98,24 @@ final class OpenEdxLmsAPITemplate implements LmsAPITemplate { .collect(Collectors.toList()); } + @Override + public Result getQuizFromCache(final String id) { + return this.openEdxCourseAccess + .getQuizFromCache(id) + .orElse(() -> getQuiz(id)); + } + @Override public Collection> getQuizzesFromCache(final Set ids) { return this.openEdxCourseAccess.getQuizzesFromCache(ids) .getOrElse(() -> getQuizzes(ids)); } + @Override + public void clearCache() { + this.openEdxCourseAccess.clearCache(); + } + @Override public Result getCourseChapters(final String courseId) { return Result.tryCatch(() -> this.openEdxCourseAccess diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxLmsAPITemplateFactory.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxLmsAPITemplateFactory.java index 1ad00b7b..a3609f16 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxLmsAPITemplateFactory.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxLmsAPITemplateFactory.java @@ -10,6 +10,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.edx; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Value; +import org.springframework.cache.CacheManager; import org.springframework.context.annotation.Lazy; import org.springframework.core.env.Environment; import org.springframework.stereotype.Service; @@ -36,6 +37,7 @@ public class OpenEdxLmsAPITemplateFactory implements LmsAPITemplateFactory { private final WebserviceInfo webserviceInfo; private final AsyncService asyncService; private final Environment environment; + private final CacheManager cacheManager; private final ClientCredentialService clientCredentialService; private final ClientHttpRequestFactoryService clientHttpRequestFactoryService; private final String[] alternativeTokenRequestPaths; @@ -46,6 +48,7 @@ public class OpenEdxLmsAPITemplateFactory implements LmsAPITemplateFactory { final WebserviceInfo webserviceInfo, final AsyncService asyncService, final Environment environment, + final CacheManager cacheManager, final ClientCredentialService clientCredentialService, final ClientHttpRequestFactoryService clientHttpRequestFactoryService, @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.asyncService = asyncService; this.environment = environment; + this.cacheManager = cacheManager; this.clientCredentialService = clientCredentialService; this.clientHttpRequestFactoryService = clientHttpRequestFactoryService; this.alternativeTokenRequestPaths = (alternativeTokenRequestPaths != null) @@ -84,7 +88,8 @@ public class OpenEdxLmsAPITemplateFactory implements LmsAPITemplateFactory { openEdxRestTemplateFactory, this.webserviceInfo, this.asyncService, - this.environment); + this.environment, + this.cacheManager); final OpenEdxCourseRestriction openEdxCourseRestriction = new OpenEdxCourseRestriction( this.jsonMapper, diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockupLmsAPITemplate.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockupLmsAPITemplate.java index cb1d22c9..90f014c8 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockupLmsAPITemplate.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockupLmsAPITemplate.java @@ -9,7 +9,9 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.mockup; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; +import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.stream.Collectors; @@ -181,6 +183,16 @@ public class MockupLmsAPITemplate implements LmsAPITemplate { return getQuizzes(ids); } + @Override + public Result getQuizFromCache(final String id) { + return getQuizzes(new HashSet<>(Arrays.asList(id))).iterator().next(); + } + + @Override + public void clearCache() { + + } + @Override public Result getCourseChapters(final String courseId) { return Result.ofError(new UnsupportedOperationException()); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleCourseAccess.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleCourseAccess.java index 7d898892..4a3d8fc8 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleCourseAccess.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleCourseAccess.java @@ -14,6 +14,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.NoSuchElementException; import java.util.Set; import java.util.function.Function; 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.async.AsyncService; 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.QuizData; 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.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.CourseQuizShort; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory.MoodleAPIRestTemplate; /** 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 MoodleCourseDataAsyncLoader moodleCourseDataAsyncLoader; private final CircuitBreaker> allQuizzesRequest; - private final AllQuizzesSupplier allQuizzesSupplier; private MoodleAPIRestTemplate restTemplate; @@ -119,19 +121,6 @@ public class MoodleCourseAccess extends AbstractCourseAccess { "sebserver.webservice.circuitbreaker.allQuizzesRequest.timeToRecover", Long.class, Constants.MINUTE_IN_MILLIS)); - - this.allQuizzesSupplier = new AllQuizzesSupplier() { - - @Override - public List getAllCached() { - return getCached(); - } - - @Override - public Result> getAll(final FilterMap filterMap) { - return MoodleCourseAccess.this.allQuizzesRequest.protectedRun(allQuizzesSupplier(filterMap)); - } - }; } APITemplateDataSupplier getApiTemplateDataSupplier() { @@ -223,6 +212,77 @@ public class MoodleCourseAccess extends AbstractCourseAccess { return LmsSetupTestResult.ofOkay(); } + @Override + public Result getQuizFromCache(final String id) { + return Result.tryCatch(() -> { + + final Map 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 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>> getQuizzesFromCache(final Set ids) { + return Result.tryCatch(() -> { + final List cached = getCached(); + final List available = (cached != null) + ? cached + : Collections.emptyList(); + + final Map quizMapping = available + .stream() + .collect(Collectors.toMap(q -> q.id, Function.identity())); + + if (!quizMapping.keySet().containsAll(ids)) { + + final Map 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. ofError(new NoSuchElementException("Quiz with id: " + id)) + : Result.of(q); + }) + .collect(Collectors.toList()); + }); + } + @Override protected Supplier> quizzesSupplier(final Set ids) { return () -> getRestTemplate() @@ -231,6 +291,7 @@ public class MoodleCourseAccess extends AbstractCourseAccess { } + @Override protected Supplier> allQuizzesSupplier(final FilterMap filterMap) { return () -> getRestTemplate() .map(template -> collectAllQuizzes(template, filterMap)) @@ -244,6 +305,9 @@ public class MoodleCourseAccess extends AbstractCourseAccess { @Override protected FetchStatus getFetchStatus() { + if (this.allQuizzesRequest.getState() != State.CLOSED) { + return FetchStatus.FETCH_ERROR; + } if (this.moodleCourseDataAsyncLoader.isRunning()) { return FetchStatus.ASYNC_FETCH_RUNNING; } @@ -251,9 +315,8 @@ public class MoodleCourseAccess extends AbstractCourseAccess { return FetchStatus.ALL_FETCHED; } - @Override - protected AllQuizzesSupplier allQuizzesSupplier() { - return this.allQuizzesSupplier; + public void clearCache() { + this.moodleCourseDataAsyncLoader.clearCache(); } private List collectAllQuizzes( @@ -261,7 +324,6 @@ public class MoodleCourseAccess extends AbstractCourseAccess { final FilterMap filterMap) { 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; @@ -272,7 +334,7 @@ public class MoodleCourseAccess extends AbstractCourseAccess { // Verify and call the proper strategy to get the course and quiz data Collection courseQuizData = Collections.emptyList(); if (this.moodleCourseDataAsyncLoader.isRunning()) { - courseQuizData = this.moodleCourseDataAsyncLoader.getCachedCourseData(); + courseQuizData = this.moodleCourseDataAsyncLoader.getCachedCourseData().values(); } else if (this.moodleCourseDataAsyncLoader.getLastRunTime() <= 0) { // set cut time if available if (fromCutTime >= 0) { @@ -282,7 +344,7 @@ public class MoodleCourseAccess extends AbstractCourseAccess { this.moodleCourseDataAsyncLoader.loadAsync(restTemplate); try { Thread.sleep(INITIAL_WAIT_TIME); - courseQuizData = this.moodleCourseDataAsyncLoader.getCachedCourseData(); + courseQuizData = this.moodleCourseDataAsyncLoader.getCachedCourseData().values(); } catch (final Exception e) { log.error("Failed to wait for first load run: ", e); return Collections.emptyList(); @@ -290,7 +352,7 @@ public class MoodleCourseAccess extends AbstractCourseAccess { } else if (this.moodleCourseDataAsyncLoader.isLongRunningTask()) { // on long running tasks if we have a different fromCutTime as before // 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()) { this.moodleCourseDataAsyncLoader.setFromCutTime(fromCutTime); this.moodleCourseDataAsyncLoader.loadAsync(restTemplate); @@ -306,7 +368,7 @@ public class MoodleCourseAccess extends AbstractCourseAccess { this.moodleCourseDataAsyncLoader.setFromCutTime(fromCutTime); } this.moodleCourseDataAsyncLoader.loadSync(restTemplate); - courseQuizData = this.moodleCourseDataAsyncLoader.getCachedCourseData(); + courseQuizData = this.moodleCourseDataAsyncLoader.getCachedCourseData().values(); } if (courseQuizData.isEmpty()) { @@ -317,7 +379,8 @@ public class MoodleCourseAccess extends AbstractCourseAccess { } private List getCached() { - final Collection courseQuizData = this.moodleCourseDataAsyncLoader.getCachedCourseData(); + final Collection courseQuizData = + this.moodleCourseDataAsyncLoader.getCachedCourseData().values(); final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup(); final String urlPrefix = (lmsSetup.lmsApiUrl.endsWith(Constants.URL_PATH_SEPARATOR)) ? lmsSetup.lmsApiUrl + MOODLE_QUIZ_START_URL_PATH @@ -534,34 +597,41 @@ public class MoodleCourseAccess extends AbstractCourseAccess { final List 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(), - 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); - }) + .map(courseQuizData -> createQuizData(lmsSetup, courseData, uriPrefix, additionalAttrs, courseQuizData)) .collect(Collectors.toList()); return courseAndQuiz; } + private QuizData createQuizData( + final LmsSetup lmsSetup, + final CourseDataShort courseData, + final String uriPrefix, + final Map 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 getRestTemplate() { if (this.restTemplate == null) { final Result templateRequest = this.moodleRestTemplateFactory diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleCourseDataAsyncLoader.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleCourseDataAsyncLoader.java index ede74cdb..4d3a9730 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleCourseDataAsyncLoader.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleCourseDataAsyncLoader.java @@ -65,7 +65,7 @@ public class MoodleCourseDataAsyncLoader { private final int maxSize; private final int pageSize; - private final Set cachedCourseData = new HashSet<>(); + private final Map cachedCourseData = new HashMap<>(); private final Set newIds = new HashSet<>(); private String lmsSetup = Constants.EMPTY_NOTE; @@ -122,8 +122,8 @@ public class MoodleCourseDataAsyncLoader { this.fromCutTime = fromCutTime; } - public Set getCachedCourseData() { - return new HashSet<>(this.cachedCourseData); + public Map getCachedCourseData() { + return new HashMap<>(this.cachedCourseData); } public long getLastRunTime() { @@ -138,7 +138,7 @@ public class MoodleCourseDataAsyncLoader { return this.lastLoadTime > 3 * Constants.SECOND_IN_MILLIS; } - public Set loadSync(final MoodleAPIRestTemplate restTemplate) { + public Map loadSync(final MoodleAPIRestTemplate restTemplate) { if (this.running) { 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) { return () -> { - //this.cachedCourseData.clear(); this.newIds.clear(); final long startTime = Utils.getMillisecondsNow(); loadAllQuizzes(restTemplate); - syncCache(); + this.syncCache(); this.lastLoadTime = Utils.getMillisecondsNow() - startTime; this.running = false; @@ -263,7 +269,7 @@ public class MoodleCourseDataAsyncLoader { this.lmsSetup, c); } else { - this.cachedCourseData.add(c); + this.cachedCourseData.put(c.id, c); this.newIds.add(c.id); } }); @@ -461,13 +467,15 @@ public class MoodleCourseDataAsyncLoader { private void syncCache() { if (!this.cachedCourseData.isEmpty()) { - final Set newData = this.cachedCourseData.stream() - .filter(data -> this.newIds.contains(data.id)) + + final Set oldData = this.cachedCourseData + .keySet() + .stream() + .filter(id -> !this.newIds.contains(id)) .collect(Collectors.toSet()); synchronized (this.cachedCourseData) { - this.cachedCourseData.clear(); - this.cachedCourseData.addAll(newData); + oldData.stream().forEach(this.cachedCourseData::remove); } } this.newIds.clear(); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleLmsAPITemplate.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleLmsAPITemplate.java index 18560d03..f4079e82 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleLmsAPITemplate.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleLmsAPITemplate.java @@ -110,6 +110,17 @@ public class MoodleLmsAPITemplate implements LmsAPITemplate { .collect(Collectors.toList()); } + @Override + public Result getQuizFromCache(final String id) { + return this.moodleCourseAccess.getQuizFromCache(id) + .orElse(() -> getQuiz(id)); + } + + @Override + public void clearCache() { + this.moodleCourseAccess.clearCache(); + } + @Override public Collection> getQuizzesFromCache(final Set ids) { return this.moodleCourseAccess.getQuizzesFromCache(ids) diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java index c65af50f..1d01a7df 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java @@ -123,7 +123,8 @@ public class ExamSessionServiceImpl implements ExamSessionService { return Result.tryCatch(() -> { final Collection result = new ArrayList<>(); - final Exam exam = this.examDAO.byPK(examId) + final Exam exam = this.examDAO + .getWithQuizDataFromCache(examId) .getOrThrow(); // check lms connection diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java index 9da295d5..6efd89c4 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java @@ -279,7 +279,8 @@ public class ExamAdministrationController extends EntityController { defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId) { checkReadPrivilege(institutionId); - return this.examDAO.byPK(modelId) + return this.examDAO + .getWithQuizDataFromCache(modelId) .flatMap(this.examAdminService::isRestricted) .getOrThrow(); } @@ -407,9 +408,7 @@ public class ExamAdministrationController extends EntityController { @PathVariable final Long modelId) { checkReadPrivilege(institutionId); - return this.entityDAO.byPK(modelId) - .flatMap(this.authorization::checkRead) - .flatMap(exam -> this.examAdminService.getProctoringServiceSettings(exam.id)) + return this.examAdminService.getProctoringServiceSettings(modelId) .getOrThrow(); } diff --git a/src/main/resources/config/ehcache.xml b/src/main/resources/config/ehcache.xml index 4d6312cd..b212c3e2 100644 --- a/src/main/resources/config/ehcache.xml +++ b/src/main/resources/config/ehcache.xml @@ -23,7 +23,7 @@ 24 - 2000 + 3000 @@ -93,7 +93,16 @@ - + + java.lang.String + ch.ethz.seb.sebserver.gbl.model.exam.QuizData + + 10 + + + 10000 + + \ No newline at end of file