Added EHcache for caching and improved Moodle asnyc loading

This commit is contained in:
anhefti 2021-01-12 10:10:30 +01:00
parent 5f30aa9c2e
commit eec4392f78
20 changed files with 685 additions and 218 deletions

14
pom.xml
View file

@ -290,11 +290,15 @@
<artifactId>flyway-core</artifactId> <artifactId>flyway-core</artifactId>
</dependency> </dependency>
<!-- JMX --> <!-- EHCache -->
<!-- <dependency> --> <dependency>
<!-- <groupId>org.jolokia</groupId> --> <groupId>org.ehcache</groupId>
<!-- <artifactId>jolokia-core</artifactId> --> <artifactId>ehcache</artifactId>
<!-- </dependency> --> </dependency>
<dependency>
<groupId>javax.cache</groupId>
<artifactId>cache-api</artifactId>
</dependency>
<!-- Apache HTTP --> <!-- Apache HTTP -->
<dependency> <dependency>

View file

@ -15,7 +15,6 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration; import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.servlet.server.ServletWebServerFactory; import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment; import org.springframework.core.env.Environment;
@ -40,7 +39,6 @@ import ch.ethz.seb.sebserver.gbl.profile.ProdWebServiceProfile;
@SpringBootApplication(exclude = { @SpringBootApplication(exclude = {
UserDetailsServiceAutoConfiguration.class, UserDetailsServiceAutoConfiguration.class,
}) })
@EnableCaching
public class SEBServer { public class SEBServer {
public static void main(final String[] args) { public static void main(final String[] args) {

View file

@ -45,6 +45,7 @@ import ch.ethz.seb.sebserver.gui.service.page.impl.ModalInputDialog;
import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction; import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.CheckExamImported; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.CheckExamImported;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.quiz.GetQuizData;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.quiz.GetQuizPage; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.quiz.GetQuizPage;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser; import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser.GrantCheck; import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser.GrantCheck;
@ -297,7 +298,11 @@ public class QuizLookupList implements TemplateComposer {
final QuizData quizData, final QuizData quizData,
final Function<String, String> institutionNameFunction) { final Function<String, String> institutionNameFunction) {
action.getSingleSelection(); final QuizData fullQuizData = this.pageService.getRestService().getBuilder(GetQuizData.class)
.withURIVariable(API.PARAM_MODEL_ID, quizData.getModelId())
.withQueryParam(QuizData.QUIZ_ATTR_LMS_SETUP_ID, String.valueOf(quizData.lmsSetupId))
.call()
.getOr(quizData);
final ModalInputDialog<Void> dialog = new ModalInputDialog<Void>( final ModalInputDialog<Void> dialog = new ModalInputDialog<Void>(
action.pageContext().getParent().getShell(), action.pageContext().getParent().getShell(),
@ -307,7 +312,7 @@ public class QuizLookupList implements TemplateComposer {
dialog.open( dialog.open(
DETAILS_TITLE_TEXT_KEY, DETAILS_TITLE_TEXT_KEY,
action.pageContext(), action.pageContext(),
pc -> createDetailsForm(quizData, pc, institutionNameFunction)); pc -> createDetailsForm(fullQuizData, pc, institutionNameFunction));
return action; return action;
} }

View file

@ -0,0 +1,66 @@
/*
* 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;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import javax.cache.Caching;
import javax.cache.spi.CachingProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
import org.springframework.cache.jcache.JCacheCacheManager;
import org.springframework.cache.jcache.config.JCacheConfigurerSupport;
import org.springframework.cache.support.CompositeCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
@EnableCaching
@WebServiceProfile
@Configuration
public class CacheConfig extends JCacheConfigurerSupport {
private static final Logger log = LoggerFactory.getLogger(CacheConfig.class);
@Value("${spring.cache.jcache.config}")
String jCacheConfig;
@Override
@Bean
public CacheManager cacheManager() {
try {
final CachingProvider cachingProvider = Caching.getCachingProvider();
final javax.cache.CacheManager cacheManager =
cachingProvider.getCacheManager(new URI(this.jCacheConfig), this.getClass().getClassLoader());
System.out.println("cacheManager:" + cacheManager);
final CompositeCacheManager composite = new CompositeCacheManager();
composite.setCacheManagers(Arrays.asList(
new JCacheCacheManager(cacheManager),
new ConcurrentMapCacheManager()));
composite.setFallbackToNoOpCache(true);
return composite;
} catch (final URISyntaxException e) {
log.error("Failed to initialize caching with EHCache. Fallback to simple caching");
return new ConcurrentMapCacheManager();
}
}
}

View file

@ -20,55 +20,81 @@ import java.util.stream.Collectors;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
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.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.user.ExamineeAccountDetails; import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
public abstract class CourseAccess { public abstract class CourseAccess {
private static final Logger log = LoggerFactory.getLogger(CourseAccess.class); private static final Logger log = LoggerFactory.getLogger(CourseAccess.class);
protected final MemoizingCircuitBreaker<List<QuizData>> allQuizzesRequest; public enum FetchStatus {
ALL_FETCHED,
ASYNC_FETCH_RUNNING,
FETCH_ERROR
}
protected final CircuitBreaker<List<QuizData>> quizzesRequest; protected final CircuitBreaker<List<QuizData>> quizzesRequest;
protected final CircuitBreaker<Chapters> chaptersRequest; protected final CircuitBreaker<Chapters> chaptersRequest;
protected final CircuitBreaker<ExamineeAccountDetails> accountDetailRequest; protected final CircuitBreaker<ExamineeAccountDetails> accountDetailRequest;
protected CourseAccess(final AsyncService asyncService) { protected CourseAccess(
this.allQuizzesRequest = asyncService.createMemoizingCircuitBreaker( final AsyncService asyncService,
allQuizzesSupplier(null), final Environment environment) {
3,
Constants.MINUTE_IN_MILLIS,
Constants.MINUTE_IN_MILLIS,
true,
Constants.HOUR_IN_MILLIS);
this.quizzesRequest = asyncService.createCircuitBreaker( this.quizzesRequest = asyncService.createCircuitBreaker(
3, environment.getProperty(
Constants.MINUTE_IN_MILLIS, "sebserver.webservice.circuitbreaker.quizzesRequest.attempts",
Constants.MINUTE_IN_MILLIS); Integer.class,
3),
environment.getProperty(
"sebserver.webservice.circuitbreaker.quizzesRequest.blockingTime",
Long.class,
Constants.MINUTE_IN_MILLIS),
environment.getProperty(
"sebserver.webservice.circuitbreaker.quizzesRequest.timeToRecover",
Long.class,
Constants.MINUTE_IN_MILLIS));
this.chaptersRequest = asyncService.createCircuitBreaker( this.chaptersRequest = asyncService.createCircuitBreaker(
3, environment.getProperty(
Constants.SECOND_IN_MILLIS * 10, "sebserver.webservice.circuitbreaker.chaptersRequest.attempts",
Constants.MINUTE_IN_MILLIS); Integer.class,
3),
environment.getProperty(
"sebserver.webservice.circuitbreaker.chaptersRequest.blockingTime",
Long.class,
Constants.SECOND_IN_MILLIS * 10),
environment.getProperty(
"sebserver.webservice.circuitbreaker.chaptersRequest.timeToRecover",
Long.class,
Constants.MINUTE_IN_MILLIS));
this.accountDetailRequest = asyncService.createCircuitBreaker( this.accountDetailRequest = asyncService.createCircuitBreaker(
1, environment.getProperty(
Constants.SECOND_IN_MILLIS * 10, "sebserver.webservice.circuitbreaker.accountDetailRequest.attempts",
Constants.SECOND_IN_MILLIS * 10); Integer.class,
2),
environment.getProperty(
"sebserver.webservice.circuitbreaker.accountDetailRequest.blockingTime",
Long.class,
Constants.SECOND_IN_MILLIS * 10),
environment.getProperty(
"sebserver.webservice.circuitbreaker.accountDetailRequest.timeToRecover",
Long.class,
Constants.SECOND_IN_MILLIS * 10));
} }
public Result<Collection<Result<QuizData>>> getQuizzesFromCache(final Set<String> ids) { public Result<Collection<Result<QuizData>>> getQuizzesFromCache(final Set<String> ids) {
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
final List<QuizData> cached = this.allQuizzesRequest.getCached(); final List<QuizData> cached = allQuizzesSupplier().getAllCached();
final List<QuizData> available = (cached != null) final List<QuizData> available = (cached != null)
? cached ? cached
: quizzesSupplier(ids).get(); : quizzesSupplier(ids).get();
@ -101,11 +127,7 @@ public abstract class CourseAccess {
} }
public Result<List<QuizData>> getQuizzes(final FilterMap filterMap) { public Result<List<QuizData>> getQuizzes(final FilterMap filterMap) {
if (filterMap != null) { return allQuizzesSupplier().getAll(filterMap);
this.allQuizzesRequest.setSupplier(allQuizzesSupplier(filterMap));
}
return this.allQuizzesRequest.get()
.map(LmsAPIService.quizzesFilterFunction(filterMap));
} }
public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeSessionId) { public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeSessionId) {
@ -139,8 +161,16 @@ public abstract class CourseAccess {
protected abstract Supplier<List<QuizData>> quizzesSupplier(final Set<String> ids); protected abstract Supplier<List<QuizData>> quizzesSupplier(final Set<String> ids);
protected abstract Supplier<List<QuizData>> allQuizzesSupplier(final FilterMap filterMap); protected abstract AllQuizzesSupplier allQuizzesSupplier();
protected abstract Supplier<Chapters> getCourseChaptersSupplier(final String courseId); protected abstract Supplier<Chapters> getCourseChaptersSupplier(final String courseId);
protected abstract FetchStatus getFetchStatus();
protected interface AllQuizzesSupplier {
List<QuizData> getAllCached();
Result<List<QuizData>> getAll(final FilterMap filterMap);
}
} }

View file

@ -20,6 +20,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.core.env.Environment;
import org.springframework.http.HttpEntity; import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
@ -41,6 +42,8 @@ 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;
@ -70,6 +73,8 @@ final class OpenEdxCourseAccess extends CourseAccess {
private final LmsSetup lmsSetup; private final LmsSetup lmsSetup;
private final OpenEdxRestTemplateFactory openEdxRestTemplateFactory; private final OpenEdxRestTemplateFactory openEdxRestTemplateFactory;
private final WebserviceInfo webserviceInfo; private final WebserviceInfo webserviceInfo;
private final MemoizingCircuitBreaker<List<QuizData>> allQuizzesRequest;
private final AllQuizzesSupplier allQuizzesSupplier;
private OAuth2RestTemplate restTemplate; private OAuth2RestTemplate restTemplate;
@ -78,13 +83,51 @@ final class OpenEdxCourseAccess extends CourseAccess {
final LmsSetup lmsSetup, final LmsSetup lmsSetup,
final OpenEdxRestTemplateFactory openEdxRestTemplateFactory, final OpenEdxRestTemplateFactory openEdxRestTemplateFactory,
final WebserviceInfo webserviceInfo, final WebserviceInfo webserviceInfo,
final AsyncService asyncService) { final AsyncService asyncService,
final Environment environment) {
super(asyncService); super(asyncService, environment);
this.jsonMapper = jsonMapper; this.jsonMapper = jsonMapper;
this.lmsSetup = lmsSetup; this.lmsSetup = lmsSetup;
this.openEdxRestTemplateFactory = openEdxRestTemplateFactory; this.openEdxRestTemplateFactory = openEdxRestTemplateFactory;
this.webserviceInfo = webserviceInfo; 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<QuizData> getAllCached() {
return OpenEdxCourseAccess.this.allQuizzesRequest.getCached();
}
@Override
public Result<List<QuizData>> getAll(final FilterMap filterMap) {
return OpenEdxCourseAccess.this.allQuizzesRequest.get();
}
};
} }
LmsSetupTestResult initAPIAccess() { LmsSetupTestResult initAPIAccess() {
@ -173,8 +216,7 @@ final class OpenEdxCourseAccess extends CourseAccess {
.getOrThrow(); .getOrThrow();
} }
@Override private Supplier<List<QuizData>> quizzesSupplier() {
protected Supplier<List<QuizData>> allQuizzesSupplier(final FilterMap filterMap) {
return () -> getRestTemplate() return () -> getRestTemplate()
.map(this::collectAllQuizzes) .map(this::collectAllQuizzes)
.getOrThrow(); .getOrThrow();
@ -451,4 +493,17 @@ final class OpenEdxCourseAccess extends CourseAccess {
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;
}
} }

View file

@ -11,6 +11,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.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import ch.ethz.seb.sebserver.ClientHttpRequestFactoryService; import ch.ethz.seb.sebserver.ClientHttpRequestFactoryService;
@ -33,6 +34,7 @@ public class OpenEdxLmsAPITemplateFactory {
private final JSONMapper jsonMapper; private final JSONMapper jsonMapper;
private final WebserviceInfo webserviceInfo; private final WebserviceInfo webserviceInfo;
private final AsyncService asyncService; private final AsyncService asyncService;
private final Environment environment;
private final ClientCredentialService clientCredentialService; private final ClientCredentialService clientCredentialService;
private final ClientHttpRequestFactoryService clientHttpRequestFactoryService; private final ClientHttpRequestFactoryService clientHttpRequestFactoryService;
private final String[] alternativeTokenRequestPaths; private final String[] alternativeTokenRequestPaths;
@ -42,6 +44,7 @@ public class OpenEdxLmsAPITemplateFactory {
final JSONMapper jsonMapper, final JSONMapper jsonMapper,
final WebserviceInfo webserviceInfo, final WebserviceInfo webserviceInfo,
final AsyncService asyncService, final AsyncService asyncService,
final Environment environment,
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,
@ -50,6 +53,7 @@ public class OpenEdxLmsAPITemplateFactory {
this.jsonMapper = jsonMapper; this.jsonMapper = jsonMapper;
this.webserviceInfo = webserviceInfo; this.webserviceInfo = webserviceInfo;
this.asyncService = asyncService; this.asyncService = asyncService;
this.environment = environment;
this.clientCredentialService = clientCredentialService; this.clientCredentialService = clientCredentialService;
this.clientHttpRequestFactoryService = clientHttpRequestFactoryService; this.clientHttpRequestFactoryService = clientHttpRequestFactoryService;
this.alternativeTokenRequestPaths = (alternativeTokenRequestPaths != null) this.alternativeTokenRequestPaths = (alternativeTokenRequestPaths != null)
@ -78,7 +82,8 @@ public class OpenEdxLmsAPITemplateFactory {
lmsSetup, lmsSetup,
openEdxRestTemplateFactory, openEdxRestTemplateFactory,
this.webserviceInfo, this.webserviceInfo,
this.asyncService); this.asyncService,
this.environment);
final OpenEdxCourseRestriction openEdxCourseRestriction = new OpenEdxCourseRestriction( final OpenEdxCourseRestriction openEdxCourseRestriction = new OpenEdxCourseRestriction(
lmsSetup, lmsSetup,

View file

@ -23,6 +23,7 @@ import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime; import org.joda.time.DateTime;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.core.env.Environment;
import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap; import org.springframework.util.MultiValueMap;
@ -34,6 +35,7 @@ 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;
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;
@ -43,6 +45,7 @@ import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gbl.util.Utils; import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.CourseAccess; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.CourseAccess;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleCourseDataAsyncLoader.CourseDataShort;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory.MoodleAPIRestTemplate; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory.MoodleAPIRestTemplate;
/** Implements the LmsAPITemplate for Open edX LMS Course API access. /** Implements the LmsAPITemplate for Open edX LMS Course API access.
@ -73,7 +76,9 @@ public class MoodleCourseAccess extends CourseAccess {
private final JSONMapper jsonMapper; private final JSONMapper jsonMapper;
private final LmsSetup lmsSetup; private final LmsSetup lmsSetup;
private final MoodleRestTemplateFactory moodleRestTemplateFactory; private final MoodleRestTemplateFactory moodleRestTemplateFactory;
private final MoodleCourseDataLazyLoader moodleCourseDataLazyLoader; private final MoodleCourseDataAsyncLoader moodleCourseDataAsyncLoader;
private final CircuitBreaker<List<QuizData>> allQuizzesRequest;
private final AllQuizzesSupplier allQuizzesSupplier;
private MoodleAPIRestTemplate restTemplate; private MoodleAPIRestTemplate restTemplate;
@ -81,14 +86,42 @@ public class MoodleCourseAccess extends CourseAccess {
final JSONMapper jsonMapper, final JSONMapper jsonMapper,
final LmsSetup lmsSetup, final LmsSetup lmsSetup,
final MoodleRestTemplateFactory moodleRestTemplateFactory, final MoodleRestTemplateFactory moodleRestTemplateFactory,
final MoodleCourseDataLazyLoader moodleCourseDataLazyLoader, final MoodleCourseDataAsyncLoader moodleCourseDataAsyncLoader,
final AsyncService asyncService) { final AsyncService asyncService,
final Environment environment) {
super(asyncService); super(asyncService, environment);
this.jsonMapper = jsonMapper; this.jsonMapper = jsonMapper;
this.lmsSetup = lmsSetup; this.lmsSetup = lmsSetup;
this.moodleCourseDataLazyLoader = moodleCourseDataLazyLoader; this.moodleCourseDataAsyncLoader = moodleCourseDataAsyncLoader;
this.moodleRestTemplateFactory = moodleRestTemplateFactory; this.moodleRestTemplateFactory = moodleRestTemplateFactory;
this.allQuizzesRequest = asyncService.createCircuitBreaker(
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));
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));
}
};
} }
@Override @Override
@ -175,7 +208,6 @@ public class MoodleCourseAccess extends CourseAccess {
} }
@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))
@ -187,6 +219,20 @@ public class MoodleCourseAccess extends CourseAccess {
throw new UnsupportedOperationException("not available yet"); throw new UnsupportedOperationException("not available yet");
} }
@Override
protected FetchStatus getFetchStatus() {
if (this.moodleCourseDataAsyncLoader.isRunning()) {
return FetchStatus.ASYNC_FETCH_RUNNING;
}
return FetchStatus.ALL_FETCHED;
}
@Override
protected AllQuizzesSupplier allQuizzesSupplier() {
return this.allQuizzesSupplier;
}
private List<QuizData> collectAllQuizzes( private List<QuizData> collectAllQuizzes(
final MoodleAPIRestTemplate restTemplate, final MoodleAPIRestTemplate restTemplate,
final FilterMap filterMap) { final FilterMap filterMap) {
@ -198,42 +244,42 @@ public class MoodleCourseAccess extends CourseAccess {
final DateTime quizFromTime = (filterMap != null) ? filterMap.getQuizFromTime() : null; final DateTime quizFromTime = (filterMap != null) ? filterMap.getQuizFromTime() : null;
final long fromCutTime = (quizFromTime != null) ? Utils.toUnixTimeInSeconds(quizFromTime) : -1; final long fromCutTime = (quizFromTime != null) ? Utils.toUnixTimeInSeconds(quizFromTime) : -1;
Collection<CourseData> courseQuizData = Collections.emptyList(); Collection<CourseDataShort> courseQuizData = Collections.emptyList();
if (this.moodleCourseDataLazyLoader.isRunning()) { if (this.moodleCourseDataAsyncLoader.isRunning()) {
courseQuizData = this.moodleCourseDataLazyLoader.getPreFilteredCourseIds(); courseQuizData = this.moodleCourseDataAsyncLoader.getCachedCourseData();
} else if (this.moodleCourseDataLazyLoader.getLastRunTime() <= 0) { } else if (this.moodleCourseDataAsyncLoader.getLastRunTime() <= 0) {
// set cut time if available // set cut time if available
if (fromCutTime >= 0) { if (fromCutTime >= 0) {
this.moodleCourseDataLazyLoader.setFromCutTime(fromCutTime); this.moodleCourseDataAsyncLoader.setFromCutTime(fromCutTime);
} }
// first run async and wait some time, get what is there // first run async and wait some time, get what is there
this.moodleCourseDataLazyLoader.loadAsync(restTemplate); this.moodleCourseDataAsyncLoader.loadAsync(restTemplate);
try { try {
Thread.sleep(INITIAL_WAIT_TIME); Thread.sleep(INITIAL_WAIT_TIME);
courseQuizData = this.moodleCourseDataLazyLoader.getPreFilteredCourseIds(); courseQuizData = this.moodleCourseDataAsyncLoader.getCachedCourseData();
} 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();
} }
} else if (this.moodleCourseDataLazyLoader.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
if (fromCutTime > 0 && fromCutTime != this.moodleCourseDataLazyLoader.getFromCutTime()) { if (fromCutTime > 0 && fromCutTime != this.moodleCourseDataAsyncLoader.getFromCutTime()) {
this.moodleCourseDataLazyLoader.setFromCutTime(fromCutTime); this.moodleCourseDataAsyncLoader.setFromCutTime(fromCutTime);
this.moodleCourseDataLazyLoader.loadAsync(restTemplate); this.moodleCourseDataAsyncLoader.loadAsync(restTemplate);
// otherwise kick off only if the last fetch task was then minutes ago // otherwise kick off only if the last fetch task was then minutes ago
} else if (Utils.getMillisecondsNow() - this.moodleCourseDataLazyLoader.getLastRunTime() > 10 } else if (Utils.getMillisecondsNow() - this.moodleCourseDataAsyncLoader.getLastRunTime() > 10
* Constants.MINUTE_IN_MILLIS) { * Constants.MINUTE_IN_MILLIS) {
this.moodleCourseDataLazyLoader.loadAsync(restTemplate); this.moodleCourseDataAsyncLoader.loadAsync(restTemplate);
} }
courseQuizData = this.moodleCourseDataLazyLoader.getPreFilteredCourseIds(); courseQuizData = this.moodleCourseDataAsyncLoader.getCachedCourseData();
} else { } else {
// just run the task in sync // just run the task in sync
if (fromCutTime >= 0) { if (fromCutTime >= 0) {
this.moodleCourseDataLazyLoader.setFromCutTime(fromCutTime); this.moodleCourseDataAsyncLoader.setFromCutTime(fromCutTime);
} }
this.moodleCourseDataLazyLoader.loadSync(restTemplate); this.moodleCourseDataAsyncLoader.loadSync(restTemplate);
courseQuizData = this.moodleCourseDataLazyLoader.getPreFilteredCourseIds(); courseQuizData = this.moodleCourseDataAsyncLoader.getCachedCourseData();
} }
if (courseQuizData.isEmpty()) { if (courseQuizData.isEmpty()) {
@ -257,6 +303,29 @@ public class MoodleCourseAccess extends CourseAccess {
}); });
} }
private List<QuizData> getCached() {
final Collection<CourseDataShort> courseQuizData = this.moodleCourseDataAsyncLoader.getCachedCourseData();
final String urlPrefix = (this.lmsSetup.lmsApiUrl.endsWith(Constants.URL_PATH_SEPARATOR))
? this.lmsSetup.lmsApiUrl + MOODLE_QUIZ_START_URL_PATH
: this.lmsSetup.lmsApiUrl + Constants.URL_PATH_SEPARATOR + MOODLE_QUIZ_START_URL_PATH;
return courseQuizData
.stream()
.reduce(
new ArrayList<>(),
(list, courseData) -> {
list.addAll(quizDataOf(
this.lmsSetup,
courseData,
urlPrefix));
return list;
},
(list1, list2) -> {
list1.addAll(list2);
return list1;
});
}
private List<QuizData> getQuizzesForIds( private List<QuizData> getQuizzesForIds(
final MoodleAPIRestTemplate restTemplate, final MoodleAPIRestTemplate restTemplate,
final Set<String> quizIds) { final Set<String> quizIds) {
@ -405,6 +474,46 @@ public class MoodleCourseAccess extends CourseAccess {
return courseAndQuiz; return courseAndQuiz;
} }
private List<QuizData> quizDataOf(
final LmsSetup lmsSetup,
final CourseDataShort courseData,
final String uriPrefix) {
additionalAttrs.clear();
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 List<QuizData> courseAndQuiz = courseData.quizzes
.stream()
.map(courseQuizData -> {
final String startURI = uriPrefix + courseQuizData.course_module;
//additionalAttrs.put(QuizData.ATTR_ADDITIONAL_TIME_LIMIT, String.valueOf(courseQuizData.time_limit));
return new QuizData(
getInternalQuizId(
courseQuizData.course_module,
courseData.id,
courseData.short_name,
courseData.idnumber),
lmsSetup.getInstitutionId(),
lmsSetup.id,
lmsSetup.getLmsType(),
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());
return courseAndQuiz;
}
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
@ -482,7 +591,7 @@ public class MoodleCourseAccess extends CourseAccess {
/** Maps the Moodle course API course data */ /** Maps the Moodle course API course data */
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
static final class CourseData { private static final class CourseData {
final String id; final String id;
final String short_name; final String short_name;
final String idnumber; final String idnumber;
@ -516,35 +625,10 @@ public class MoodleCourseAccess extends CourseAccess {
this.end_date = end_date; this.end_date = end_date;
this.time_created = time_created; this.time_created = time_created;
} }
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((this.id == null) ? 0 : this.id.hashCode());
return result;
}
@Override
public boolean equals(final Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
final CourseData other = (CourseData) obj;
if (this.id == null) {
if (other.id != null)
return false;
} else if (!this.id.equals(other.id))
return false;
return true;
}
} }
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
static final class Courses { private static final class Courses {
final Collection<CourseData> courses; final Collection<CourseData> courses;
@JsonCreator @JsonCreator
@ -555,7 +639,7 @@ public class MoodleCourseAccess extends CourseAccess {
} }
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
static final class CourseQuizData { private static final class CourseQuizData {
final Collection<CourseQuiz> quizzes; final Collection<CourseQuiz> quizzes;
@JsonCreator @JsonCreator

View file

@ -28,8 +28,10 @@ import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Scope; import org.springframework.context.annotation.Scope;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@ -40,40 +42,72 @@ import com.fasterxml.jackson.databind.JsonMappingException;
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.AsyncRunner; import ch.ethz.seb.sebserver.gbl.async.AsyncRunner;
import ch.ethz.seb.sebserver.gbl.async.AsyncService;
import ch.ethz.seb.sebserver.gbl.async.CircuitBreaker;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Utils; import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleCourseAccess.CourseData;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleCourseAccess.CourseQuiz;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleCourseAccess.CourseQuizData;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleCourseAccess.Courses;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory.MoodleAPIRestTemplate; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory.MoodleAPIRestTemplate;
@Lazy @Lazy
@Component @Component
@WebServiceProfile @WebServiceProfile
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class MoodleCourseDataLazyLoader { public class MoodleCourseDataAsyncLoader {
private static final Logger log = LoggerFactory.getLogger(MoodleCourseDataLazyLoader.class); private static final Logger log = LoggerFactory.getLogger(MoodleCourseDataAsyncLoader.class);
private final JSONMapper jsonMapper; private final JSONMapper jsonMapper;
private final AsyncRunner asyncRunner; private final AsyncRunner asyncRunner;
private final CircuitBreaker<String> moodleRestCall;
private final int maxSize;
private final int pageSize;
private final Set<CourseData> preFilteredCourseIds = new HashSet<>(); private final Set<CourseDataShort> cachedCourseData = new HashSet<>();
private String lmsSetup = Constants.EMPTY_NOTE;
private long lastRunTime = 0; private long lastRunTime = 0;
private long lastLoadTime = 0; private long lastLoadTime = 0;
private boolean running = false; private boolean running = false;
private long fromCutTime; private long fromCutTime;
public MoodleCourseDataLazyLoader( public MoodleCourseDataAsyncLoader(
final JSONMapper jsonMapper, final JSONMapper jsonMapper,
final AsyncRunner asyncRunner) { final AsyncService asyncService,
final AsyncRunner asyncRunner,
final Environment environment) {
this.jsonMapper = jsonMapper; this.jsonMapper = jsonMapper;
this.asyncRunner = asyncRunner;
this.fromCutTime = Utils.toUnixTimeInSeconds(DateTime.now(DateTimeZone.UTC).minusYears(3)); this.fromCutTime = Utils.toUnixTimeInSeconds(DateTime.now(DateTimeZone.UTC).minusYears(3));
this.asyncRunner = asyncRunner;
this.moodleRestCall = asyncService.createCircuitBreaker(
environment.getProperty(
"sebserver.webservice.circuitbreaker.moodleRestCall.attempts",
Integer.class,
2),
environment.getProperty(
"sebserver.webservice.circuitbreaker.moodleRestCall.blockingTime",
Long.class,
Constants.SECOND_IN_MILLIS * 20),
environment.getProperty(
"sebserver.webservice.circuitbreaker.moodleRestCall.timeToRecover",
Long.class,
Constants.MINUTE_IN_MILLIS));
this.maxSize =
environment.getProperty("sebserver.webservice.cache.moodle.course.maxSize", Integer.class, 10000);
this.pageSize =
environment.getProperty("sebserver.webservice.cache.moodle.course.pageSize", Integer.class, 500);
}
public void init(final String lmsSetupName) {
if (Constants.EMPTY_NOTE.equals(this.lmsSetup)) {
this.lmsSetup = lmsSetupName;
} else {
throw new IllegalStateException(
"Invalid initialization of MoodleCourseDataAsyncLoader. It has already been initialized yet");
}
} }
public long getFromCutTime() { public long getFromCutTime() {
@ -84,8 +118,8 @@ public class MoodleCourseDataLazyLoader {
this.fromCutTime = fromCutTime; this.fromCutTime = fromCutTime;
} }
public Set<CourseData> getPreFilteredCourseIds() { public Set<CourseDataShort> getCachedCourseData() {
return this.preFilteredCourseIds; return new HashSet<>(this.cachedCourseData);
} }
public long getLastRunTime() { public long getLastRunTime() {
@ -100,7 +134,7 @@ public class MoodleCourseDataLazyLoader {
return this.lastLoadTime > 30 * Constants.SECOND_IN_MILLIS; return this.lastLoadTime > 30 * Constants.SECOND_IN_MILLIS;
} }
public Set<CourseData> loadSync(final MoodleAPIRestTemplate restTemplate) { public Set<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");
} }
@ -109,9 +143,11 @@ public class MoodleCourseDataLazyLoader {
loadAndCache(restTemplate).run(); loadAndCache(restTemplate).run();
this.lastRunTime = Utils.getMillisecondsNow(); this.lastRunTime = Utils.getMillisecondsNow();
log.info("Loaded {} courses synchronously", this.preFilteredCourseIds.size()); log.info("LMS Setup: {} loaded {} courses synchronously",
this.lmsSetup,
this.cachedCourseData.size());
return this.preFilteredCourseIds; return this.cachedCourseData;
} }
public void loadAsync(final MoodleAPIRestTemplate restTemplate) { public void loadAsync(final MoodleAPIRestTemplate restTemplate) {
@ -126,6 +162,7 @@ public class MoodleCourseDataLazyLoader {
private Runnable loadAndCache(final MoodleAPIRestTemplate restTemplate) { private Runnable loadAndCache(final MoodleAPIRestTemplate restTemplate) {
return () -> { return () -> {
this.cachedCourseData.clear();
final long startTime = Utils.getMillisecondsNow(); final long startTime = Utils.getMillisecondsNow();
loadAllQuizzes(restTemplate); loadAllQuizzes(restTemplate);
@ -133,7 +170,9 @@ public class MoodleCourseDataLazyLoader {
this.lastLoadTime = Utils.getMillisecondsNow() - startTime; this.lastLoadTime = Utils.getMillisecondsNow() - startTime;
this.running = false; this.running = false;
log.info("Loaded {} courses asynchronously", this.preFilteredCourseIds.size()); log.info("LMS Setup: {} loaded {} courses asynchronously",
this.lmsSetup,
this.cachedCourseData.size());
}; };
} }
@ -151,8 +190,8 @@ public class MoodleCourseDataLazyLoader {
try { try {
// first get courses from Moodle for page // first get courses from Moodle for page
final Map<String, CourseData> courseData = new HashMap<>(); final Map<String, CourseDataShort> courseData = new HashMap<>();
final Collection<CourseData> coursesPage = getCoursesPage(restTemplate, page, 1000); final Collection<CourseDataShort> coursesPage = getCoursesPage(restTemplate, page, this.pageSize);
if (coursesPage == null || coursesPage.isEmpty()) { if (coursesPage == null || coursesPage.isEmpty()) {
return false; return false;
@ -168,7 +207,8 @@ public class MoodleCourseDataLazyLoader {
MoodleCourseAccess.MOODLE_COURSE_API_COURSE_IDS, MoodleCourseAccess.MOODLE_COURSE_API_COURSE_IDS,
new ArrayList<>(courseData.keySet())); new ArrayList<>(courseData.keySet()));
final String quizzesJSON = restTemplate.callMoodleAPIFunction( final String quizzesJSON = callMoodleRestAPI(
restTemplate,
MoodleCourseAccess.MOODLE_QUIZ_API_FUNCTION_NAME, MoodleCourseAccess.MOODLE_QUIZ_API_FUNCTION_NAME,
attributes); attributes);
@ -185,28 +225,36 @@ public class MoodleCourseDataLazyLoader {
.stream() .stream()
.filter(getQuizFilter()) .filter(getQuizFilter())
.forEach(quiz -> { .forEach(quiz -> {
final CourseData data = courseData.get(quiz.course); final CourseDataShort data = courseData.get(quiz.course);
if (data != null) { if (data != null) {
data.quizzes.add(quiz); data.quizzes.add(quiz);
} }
}); });
this.preFilteredCourseIds.addAll(
courseData.values().stream() courseData.values().stream()
.filter(c -> !c.quizzes.isEmpty()) .filter(c -> !c.quizzes.isEmpty())
.collect(Collectors.toList())); .forEach(c -> {
if (this.cachedCourseData.size() >= this.maxSize) {
log.error(
"LMS Setup: {} Cache is full and has reached its maximal size. Skip data: -> {}",
this.lmsSetup,
c);
} else {
this.cachedCourseData.add(c);
}
});
return true; return true;
} else { } else {
return false; return false;
} }
} catch (final Exception e) { } catch (final Exception e) {
log.error("Unexpected exception while trying to get course data: ", e); log.error("LMS Setup: {} Unexpected exception while trying to get course data: ", this.lmsSetup, e);
return false; return false;
} }
} }
private Collection<CourseData> getCoursesPage( private Collection<CourseDataShort> getCoursesPage(
final MoodleAPIRestTemplate restTemplate, final MoodleAPIRestTemplate restTemplate,
final int page, final int page,
final int size) throws JsonParseException, JsonMappingException, IOException { final int size) throws JsonParseException, JsonMappingException, IOException {
@ -219,7 +267,8 @@ public class MoodleCourseDataLazyLoader {
attributes.add(MoodleCourseAccess.MOODLE_COURSE_API_SEARCH_PAGE, String.valueOf(page)); attributes.add(MoodleCourseAccess.MOODLE_COURSE_API_SEARCH_PAGE, String.valueOf(page));
attributes.add(MoodleCourseAccess.MOODLE_COURSE_API_SEARCH_PAGE_SIZE, String.valueOf(size)); attributes.add(MoodleCourseAccess.MOODLE_COURSE_API_SEARCH_PAGE_SIZE, String.valueOf(size));
final String courseKeyPageJSON = restTemplate.callMoodleAPIFunction( final String courseKeyPageJSON = callMoodleRestAPI(
restTemplate,
MoodleCourseAccess.MOODLE_COURSE_SEARCH_API_FUNCTION_NAME, MoodleCourseAccess.MOODLE_COURSE_SEARCH_API_FUNCTION_NAME,
attributes); attributes);
@ -228,7 +277,9 @@ public class MoodleCourseDataLazyLoader {
CoursePage.class); CoursePage.class);
if (keysPage == null || keysPage.courseKeys == null || keysPage.courseKeys.isEmpty()) { if (keysPage == null || keysPage.courseKeys == null || keysPage.courseKeys.isEmpty()) {
log.info("No courses found on page: {}", page); if (log.isDebugEnabled()) {
log.debug("LMS Setup: {} No courses found on page: {}", this.lmsSetup, page);
}
return Collections.emptyList(); return Collections.emptyList();
} }
@ -238,7 +289,7 @@ public class MoodleCourseDataLazyLoader {
.map(key -> key.id) .map(key -> key.id)
.collect(Collectors.toSet()); .collect(Collectors.toSet());
final Collection<CourseData> result = getCoursesForIds(restTemplate, ids) final Collection<CourseDataShort> result = getCoursesForIds(restTemplate, ids)
.stream() .stream()
.filter(getCourseFilter()) .filter(getCourseFilter())
.collect(Collectors.toList()); .collect(Collectors.toList());
@ -247,19 +298,19 @@ public class MoodleCourseDataLazyLoader {
return result; return result;
} catch (final Exception e) { } catch (final Exception e) {
log.error("Unexpected error while trying to get courses page: ", e); log.error("LMS Setup: {} Unexpected error while trying to get courses page: ", this.lmsSetup, e);
return Collections.emptyList(); return Collections.emptyList();
} }
} }
private Collection<CourseData> getCoursesForIds( private Collection<CourseDataShort> getCoursesForIds(
final MoodleAPIRestTemplate restTemplate, final MoodleAPIRestTemplate restTemplate,
final Set<String> ids) { final Set<String> ids) {
try { try {
if (log.isDebugEnabled()) { if (log.isDebugEnabled()) {
log.debug("Get courses for ids: {}", ids); log.debug("LMS Setup: {} Get courses for ids: {}", this.lmsSetup, ids);
} }
final String joinedIds = StringUtils.join(ids, Constants.COMMA); final String joinedIds = StringUtils.join(ids, Constants.COMMA);
@ -267,32 +318,52 @@ public class MoodleCourseDataLazyLoader {
final LinkedMultiValueMap<String, String> attributes = new LinkedMultiValueMap<>(); final LinkedMultiValueMap<String, String> attributes = new LinkedMultiValueMap<>();
attributes.add(MoodleCourseAccess.MOODLE_COURSE_API_FIELD_NAME, MoodleCourseAccess.MOODLE_COURSE_API_IDS); attributes.add(MoodleCourseAccess.MOODLE_COURSE_API_FIELD_NAME, MoodleCourseAccess.MOODLE_COURSE_API_IDS);
attributes.add(MoodleCourseAccess.MOODLE_COURSE_API_FIELD_VALUE, joinedIds); attributes.add(MoodleCourseAccess.MOODLE_COURSE_API_FIELD_VALUE, joinedIds);
final String coursePageJSON = restTemplate.callMoodleAPIFunction( final String coursePageJSON = callMoodleRestAPI(
restTemplate,
MoodleCourseAccess.MOODLE_COURSE_BY_FIELD_API_FUNCTION_NAME, MoodleCourseAccess.MOODLE_COURSE_BY_FIELD_API_FUNCTION_NAME,
attributes); attributes);
return this.jsonMapper.<Courses> readValue( return this.jsonMapper.readValue(
coursePageJSON, coursePageJSON,
Courses.class).courses; Courses.class).courses;
} catch (final Exception e) { } catch (final Exception e) {
log.error("Unexpected error while trying to get courses for ids", e); log.error("LMS Setup: {} Unexpected error while trying to get courses for ids", this.lmsSetup, e);
return Collections.emptyList(); return Collections.emptyList();
} }
} }
private Predicate<CourseQuiz> getQuizFilter() { private String callMoodleRestAPI(
final MoodleAPIRestTemplate restTemplate,
final String function,
final MultiValueMap<String, String> queryAttributes) {
return this.moodleRestCall
.protectedRun(() -> restTemplate.callMoodleAPIFunction(
function,
queryAttributes))
.getOrThrow();
}
private Predicate<CourseQuizShort> getQuizFilter() {
final long now = Utils.getSecondsNow(); final long now = Utils.getSecondsNow();
return quiz -> { return quiz -> {
if (quiz.time_close == null || quiz.time_close == 0 || quiz.time_close > now) { if (quiz.time_close == null || quiz.time_close == 0 || quiz.time_close > now) {
return true; return true;
} }
log.info("remove quiz {} end_time {} now {}", quiz.name, quiz.time_close, now); if (log.isDebugEnabled()) {
log.debug("LMS Setup: {} remove quiz {} end_time {} now {}",
this.lmsSetup,
quiz.name,
quiz.time_close,
now);
}
return false; return false;
}; };
} }
private Predicate<CourseData> getCourseFilter() { private Predicate<CourseDataShort> getCourseFilter() {
final long now = Utils.getSecondsNow(); final long now = Utils.getSecondsNow();
return course -> { return course -> {
if (course.start_date < this.fromCutTime) { if (course.start_date < this.fromCutTime) {
@ -303,7 +374,13 @@ public class MoodleCourseDataLazyLoader {
return true; return true;
} }
log.info("remove course {} end_time {} now {}", course.short_name, course.end_date, now); if (log.isDebugEnabled()) {
log.info("LMS Setup: {} remove course {} end_time {} now {}",
this.lmsSetup,
course.short_name,
course.end_date,
now);
}
return false; return false;
}; };
} }
@ -356,77 +433,107 @@ public class MoodleCourseDataLazyLoader {
} }
// @JsonIgnoreProperties(ignoreUnknown = true) /** Maps the Moodle course API course data */
// static final class CourseKeys { @JsonIgnoreProperties(ignoreUnknown = true)
// final Collection<CourseDataKey> courses; static final class CourseDataShort {
// final String id;
// @JsonCreator final String short_name;
// protected CourseKeys( final String idnumber;
// @JsonProperty(value = "courses") final Collection<CourseDataKey> courses) { final Long start_date; // unix-time seconds UTC
// this.courses = courses; final Long end_date; // unix-time seconds UTC
// } final Long time_created; // unix-time seconds UTC
// } final Collection<CourseQuizShort> quizzes = new ArrayList<>();
// /** Maps the Moodle course API course data */ @JsonCreator
// @JsonIgnoreProperties(ignoreUnknown = true) protected CourseDataShort(
// static final class CourseDataKey { @JsonProperty(value = "id") final String id,
// final String id; @JsonProperty(value = "shortname") final String short_name,
// final String short_name; @JsonProperty(value = "idnumber") final String idnumber,
// final Long start_date; // unix-time seconds UTC @JsonProperty(value = "startdate") final Long start_date,
// final Long end_date; // unix-time seconds UTC @JsonProperty(value = "enddate") final Long end_date,
// final Long time_created; // unix-time seconds UTC @JsonProperty(value = "timecreated") final Long time_created) {
// final Collection<CourseQuizKey> quizzes = new ArrayList<>();
//
// @JsonCreator
// protected CourseDataKey(
// @JsonProperty(value = "id") final String id,
// @JsonProperty(value = "shortname") final String short_name,
// @JsonProperty(value = "startdate") final Long start_date,
// @JsonProperty(value = "enddate") final Long end_date,
// @JsonProperty(value = "timecreated") final Long time_created) {
//
// this.id = id;
// this.short_name = short_name;
// this.start_date = start_date;
// this.end_date = end_date;
// this.time_created = time_created;
// }
//
// }
// @JsonIgnoreProperties(ignoreUnknown = true) this.id = id;
// static final class CourseQuizKeys { this.short_name = short_name;
// final Collection<CourseQuizKey> quizzes; this.idnumber = idnumber;
// this.start_date = start_date;
// @JsonCreator this.end_date = end_date;
// protected CourseQuizKeys( this.time_created = time_created;
// @JsonProperty(value = "quizzes") final Collection<CourseQuizKey> quizzes) { }
// this.quizzes = quizzes;
// } @Override
// } public int hashCode() {
// final int prime = 31;
// @JsonIgnoreProperties(ignoreUnknown = true) int result = 1;
// static final class CourseQuizKey { result = prime * result + ((this.id == null) ? 0 : this.id.hashCode());
// final String id; return result;
// final String course; }
// final String name;
// final Long time_open; // unix-time seconds UTC @Override
// final Long time_close; // unix-time seconds UTC public boolean equals(final Object obj) {
// if (this == obj)
// @JsonCreator return true;
// protected CourseQuizKey( if (obj == null)
// @JsonProperty(value = "id") final String id, return false;
// @JsonProperty(value = "course") final String course, if (getClass() != obj.getClass())
// @JsonProperty(value = "name") final String name, return false;
// @JsonProperty(value = "timeopen") final Long time_open, final CourseDataShort other = (CourseDataShort) obj;
// @JsonProperty(value = "timeclose") final Long time_close) { if (this.id == null) {
// if (other.id != null)
// this.id = id; return false;
// this.course = course; } else if (!this.id.equals(other.id))
// this.name = name; return false;
// this.time_open = time_open; return true;
// this.time_close = time_close; }
// } }
// }
@JsonIgnoreProperties(ignoreUnknown = true)
private static final class Courses {
final Collection<CourseDataShort> courses;
@JsonCreator
protected Courses(
@JsonProperty(value = "courses") final Collection<CourseDataShort> courses) {
this.courses = courses;
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
static final class CourseQuizData {
final Collection<CourseQuizShort> quizzes;
@JsonCreator
protected CourseQuizData(
@JsonProperty(value = "quizzes") final Collection<CourseQuizShort> quizzes) {
this.quizzes = quizzes;
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
static final class CourseQuizShort {
final String id;
final String course;
final String course_module;
final String name;
final Long time_open; // unix-time seconds UTC
final Long time_close; // unix-time seconds UTC
@JsonCreator
protected CourseQuizShort(
@JsonProperty(value = "id") final String id,
@JsonProperty(value = "course") final String course,
@JsonProperty(value = "coursemodule") final String course_module,
@JsonProperty(value = "name") final String name,
@JsonProperty(value = "timeopen") final Long time_open,
@JsonProperty(value = "timeclose") final Long time_close) {
this.id = id;
this.course = course;
this.course_module = course_module;
this.name = name;
this.time_open = time_open;
this.time_close = time_close;
}
}
} }

View file

@ -12,6 +12,7 @@ import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import ch.ethz.seb.sebserver.ClientHttpRequestFactoryService; import ch.ethz.seb.sebserver.ClientHttpRequestFactoryService;
@ -32,6 +33,7 @@ public class MoodleLmsAPITemplateFactory {
private final JSONMapper jsonMapper; private final JSONMapper jsonMapper;
private final AsyncService asyncService; private final AsyncService asyncService;
private final Environment environment;
private final ClientCredentialService clientCredentialService; private final ClientCredentialService clientCredentialService;
private final ClientHttpRequestFactoryService clientHttpRequestFactoryService; private final ClientHttpRequestFactoryService clientHttpRequestFactoryService;
private final ApplicationContext applicationContext; private final ApplicationContext applicationContext;
@ -40,6 +42,7 @@ public class MoodleLmsAPITemplateFactory {
protected MoodleLmsAPITemplateFactory( protected MoodleLmsAPITemplateFactory(
final JSONMapper jsonMapper, final JSONMapper jsonMapper,
final AsyncService asyncService, final AsyncService asyncService,
final Environment environment,
final ClientCredentialService clientCredentialService, final ClientCredentialService clientCredentialService,
final ClientHttpRequestFactoryService clientHttpRequestFactoryService, final ClientHttpRequestFactoryService clientHttpRequestFactoryService,
final ApplicationContext applicationContext, final ApplicationContext applicationContext,
@ -47,6 +50,7 @@ public class MoodleLmsAPITemplateFactory {
this.jsonMapper = jsonMapper; this.jsonMapper = jsonMapper;
this.asyncService = asyncService; this.asyncService = asyncService;
this.environment = environment;
this.clientCredentialService = clientCredentialService; this.clientCredentialService = clientCredentialService;
this.clientHttpRequestFactoryService = clientHttpRequestFactoryService; this.clientHttpRequestFactoryService = clientHttpRequestFactoryService;
this.applicationContext = applicationContext; this.applicationContext = applicationContext;
@ -62,8 +66,9 @@ public class MoodleLmsAPITemplateFactory {
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
final MoodleCourseDataLazyLoader lazyLoaderPrototype = final MoodleCourseDataAsyncLoader asyncLoaderPrototype =
this.applicationContext.getBean(MoodleCourseDataLazyLoader.class); this.applicationContext.getBean(MoodleCourseDataAsyncLoader.class);
asyncLoaderPrototype.init(lmsSetup.name);
final MoodleRestTemplateFactory moodleRestTemplateFactory = new MoodleRestTemplateFactory( final MoodleRestTemplateFactory moodleRestTemplateFactory = new MoodleRestTemplateFactory(
this.jsonMapper, this.jsonMapper,
@ -78,8 +83,9 @@ public class MoodleLmsAPITemplateFactory {
this.jsonMapper, this.jsonMapper,
lmsSetup, lmsSetup,
moodleRestTemplateFactory, moodleRestTemplateFactory,
lazyLoaderPrototype, asyncLoaderPrototype,
this.asyncService); this.asyncService,
this.environment);
final MoodleCourseRestriction moodleCourseRestriction = new MoodleCourseRestriction( final MoodleCourseRestriction moodleCourseRestriction = new MoodleCourseRestriction(
this.jsonMapper, this.jsonMapper,

View file

@ -489,9 +489,7 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
.filter(Objects::nonNull) .filter(Objects::nonNull)
.filter(connection -> connection.pingIndicator != null && .filter(connection -> connection.pingIndicator != null &&
connection.clientConnection.status.establishedStatus) connection.clientConnection.status.establishedStatus)
.map(connection -> connection.pingIndicator.updateLogEvent()) .forEach(connection -> connection.pingIndicator.updateLogEvent());
.filter(Objects::nonNull)
.forEach(this.eventHandlingStrategy);
} catch (final Exception e) { } catch (final Exception e) {
log.error("Failed to update ping events: ", e); log.error("Failed to update ping events: ", e);

View file

@ -42,7 +42,7 @@ public class SEBClientNotificationServiceImpl implements SEBClientNotificationSe
private final ClientEventDAO clientEventDAO; private final ClientEventDAO clientEventDAO;
private final SEBClientInstructionService sebClientInstructionService; private final SEBClientInstructionService sebClientInstructionService;
private final Set<Long> pendingNotifications; private final Set<Long> pendingNotifications = new HashSet<>();
public SEBClientNotificationServiceImpl( public SEBClientNotificationServiceImpl(
final ClientEventDAO clientEventDAO, final ClientEventDAO clientEventDAO,
@ -50,19 +50,25 @@ public class SEBClientNotificationServiceImpl implements SEBClientNotificationSe
this.clientEventDAO = clientEventDAO; this.clientEventDAO = clientEventDAO;
this.sebClientInstructionService = sebClientInstructionService; this.sebClientInstructionService = sebClientInstructionService;
this.pendingNotifications = new HashSet<>();
} }
@Override @Override
public Boolean hasAnyPendingNotification(final Long clientConnectionId) { public Boolean hasAnyPendingNotification(final Long clientConnectionId) {
if (this.pendingNotifications.contains(clientConnectionId)) {
if (this.pendingNotifications.add(clientConnectionId)) {
return true; return true;
} }
final boolean hasAnyPendingNotification = !getPendingNotifications(clientConnectionId) final boolean hasAnyPendingNotification = !getPendingNotifications(clientConnectionId)
.getOr(Collections.emptyList()) .getOr(Collections.emptyList())
.isEmpty(); .isEmpty();
if (hasAnyPendingNotification) { if (hasAnyPendingNotification) {
// NOTE this is a quick and dirty way to keep cache pendingNotifications cache size short.
// TODO find a better way to do this.
if (this.pendingNotifications.size() > 100) {
this.pendingNotifications.clear();
}
this.pendingNotifications.add(clientConnectionId); this.pendingNotifications.add(clientConnectionId);
} }

View file

@ -1,8 +1,6 @@
server.address=localhost server.address=localhost
server.port=8090 server.port=8090
#logging.file=log/sebserver.log
# data source configuration # data source configuration
spring.datasource.initialize=true spring.datasource.initialize=true
spring.datasource.initialization-mode=always spring.datasource.initialization-mode=always

View file

@ -6,6 +6,8 @@ server.servlet.context-path=/
server.tomcat.uri-encoding=UTF-8 server.tomcat.uri-encoding=UTF-8
logging.level.ch=INFO logging.level.ch=INFO
logging.level.org.springframework.cache=INFO
logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl=DEBUG
sebserver.http.client.connect-timeout=150000 sebserver.http.client.connect-timeout=150000
sebserver.http.client.connection-request-timeout=100000 sebserver.http.client.connection-request-timeout=100000

View file

@ -8,6 +8,10 @@ sebserver.init.adminaccount.gen-on-init=true
sebserver.init.organisation.name=SEB Server sebserver.init.organisation.name=SEB Server
sebserver.init.adminaccount.username=sebserver-admin sebserver.init.adminaccount.username=sebserver-admin
### webservice caching
spring.cache.jcache.provider=org.ehcache.jsr107.EhcacheCachingProvider
spring.cache.jcache.config=classpath:config/ehcache.xml
### webservice data source configuration ### webservice data source configuration
spring.datasource.username=root spring.datasource.username=root
spring.datasource.initialize=true spring.datasource.initialize=true

View file

@ -13,7 +13,7 @@ server.port=8080
server.servlet.context-path=/ server.servlet.context-path=/
# Tomcat # Tomcat
server.tomcat.max-threads=1000 server.tomcat.max-threads=2000
server.tomcat.accept-count=300 server.tomcat.accept-count=300
server.tomcat.uri-encoding=UTF-8 server.tomcat.uri-encoding=UTF-8

View file

@ -0,0 +1,85 @@
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.ehcache.org/v3"
xmlns:jsr107="http://www.ehcache.org/v3/jsr107"
xsi:schemaLocation="
http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core-3.0.xsd
http://www.ehcache.org/v3/jsr107 http://www.ehcache.org/schema/ehcache-107-ext-3.0.xsd">
<cache alias="RUNNING_EXAM">
<key-type>java.lang.Long</key-type>
<value-type>ch.ethz.seb.sebserver.gbl.model.exam.Exam</value-type>
<expiry>
<tti unit="hours">24</tti>
</expiry>
<resources>
<heap unit="entries">100</heap>
</resources>
</cache>
<cache alias="ACTIVE_CLIENT_CONNECTION">
<key-type>java.lang.String</key-type>
<value-type>ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.ClientConnectionDataInternal</value-type>
<expiry>
<tti unit="hours">24</tti>
</expiry>
<resources>
<heap unit="entries">2000</heap>
</resources>
</cache>
<cache alias="SEB_CONFIG_EXAM">
<key-type>java.lang.Long</key-type>
<value-type>ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.InMemorySEBConfig</value-type>
<expiry>
<tti unit="hours">24</tti>
</expiry>
<resources>
<heap unit="entries">20</heap>
</resources>
</cache>
<cache alias="CACHE_NAME_PING_RECORD">
<key-type>java.lang.String</key-type>
<value-type>ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientEventRecord</value-type>
<expiry>
<tti unit="hours">24</tti>
</expiry>
<resources>
<heap unit="entries">2000</heap>
</resources>
</cache>
<cache alias="CONNECTION_TOKENS_CACHE">
<key-type>java.lang.Long</key-type>
<value-type>ch.ethz.seb.sebserver.gbl.util.Result</value-type>
<expiry>
<tti unit="hours">24</tti>
</expiry>
<resources>
<heap unit="entries">100</heap>
</resources>
</cache>
<cache alias="ACCESS_TOKEN_STORE_CACHE">
<key-type>org.springframework.security.oauth2.common.OAuth2AccessToken</key-type>
<value-type>org.springframework.security.oauth2.provider.OAuth2Authentication</value-type>
<expiry>
<tti unit="hours">24</tti>
</expiry>
<resources>
<heap unit="entries">100</heap>
</resources>
</cache>
<cache alias="EXAM_CLIENT_DETAILS_CACHE">
<key-type>java.lang.String</key-type>
<value-type>ch.ethz.seb.sebserver.gbl.util.Result</value-type>
<expiry>
<tti unit="hours">24</tti>
</expiry>
<resources>
<heap unit="entries">2000</heap>
</resources>
</cache>
</config>

View file

@ -19,6 +19,7 @@
<root level="DEBUG" additivity="true"> <root level="DEBUG" additivity="true">
<appender-ref ref="STDOUT" /> <appender-ref ref="STDOUT" />
<appender-ref ref="FILE" />
</root> </root>
<Logger name="ch.ethz.seb.SEB_SERVER_INIT" level="INFO" additivity="false"> <Logger name="ch.ethz.seb.SEB_SERVER_INIT" level="INFO" additivity="false">
<appender-ref ref="STDOUT" /> <appender-ref ref="STDOUT" />

View file

@ -15,6 +15,9 @@ import static org.mockito.Mockito.*;
import java.util.TreeMap; import java.util.TreeMap;
import org.junit.Test; import org.junit.Test;
import org.mockito.Mock;
import org.springframework.core.env.Environment;
import org.springframework.mock.env.MockEnvironment;
import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap; import org.springframework.util.MultiValueMap;
@ -28,6 +31,9 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestT
public class MoodleCourseAccessTest { public class MoodleCourseAccessTest {
@Mock
Environment env = new MockEnvironment();
@Test @Test
public void testGetExamineeAccountDetails() { public void testGetExamineeAccountDetails() {
@ -68,7 +74,8 @@ public class MoodleCourseAccessTest {
null, null,
moodleRestTemplateFactory, moodleRestTemplateFactory,
null, null,
mock(AsyncService.class)); mock(AsyncService.class),
this.env);
final String examId = "123"; final String examId = "123";
final Result<ExamineeAccountDetails> examineeAccountDetails = final Result<ExamineeAccountDetails> examineeAccountDetails =
@ -116,7 +123,8 @@ public class MoodleCourseAccessTest {
null, null,
moodleRestTemplateFactory, moodleRestTemplateFactory,
null, null,
mock(AsyncService.class)); mock(AsyncService.class),
this.env);
final LmsSetupTestResult initAPIAccess = moodleCourseAccess.initAPIAccess(); final LmsSetupTestResult initAPIAccess = moodleCourseAccess.initAPIAccess();
assertNotNull(initAPIAccess); assertNotNull(initAPIAccess);
@ -138,7 +146,8 @@ public class MoodleCourseAccessTest {
null, null,
moodleRestTemplateFactory, moodleRestTemplateFactory,
null, null,
mock(AsyncService.class)); mock(AsyncService.class),
this.env);
final LmsSetupTestResult initAPIAccess = moodleCourseAccess.initAPIAccess(); final LmsSetupTestResult initAPIAccess = moodleCourseAccess.initAPIAccess();
assertNotNull(initAPIAccess); assertNotNull(initAPIAccess);
@ -159,7 +168,8 @@ public class MoodleCourseAccessTest {
null, null,
moodleRestTemplateFactory, moodleRestTemplateFactory,
null, null,
mock(AsyncService.class)); mock(AsyncService.class),
this.env);
final LmsSetupTestResult initAPIAccess = moodleCourseAccess.initAPIAccess(); final LmsSetupTestResult initAPIAccess = moodleCourseAccess.initAPIAccess();
assertNotNull(initAPIAccess); assertNotNull(initAPIAccess);

View file

@ -12,6 +12,9 @@ server.servlet.context-path=/
spring.main.allow-bean-definition-overriding=true spring.main.allow-bean-definition-overriding=true
sebserver.password=test-password sebserver.password=test-password
spring.cache.jcache.provider=
spring.cache.jcache.config=classpath:config/ehcache.xml
spring.h2.console.enabled=true spring.h2.console.enabled=true
spring.datasource.platform=h2 spring.datasource.platform=h2
spring.datasource.url=jdbc:h2:mem:test;MODE=MySQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE spring.datasource.url=jdbc:h2:mem:test;MODE=MySQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE