SEBSERV-417 new Moodle POST API calls

This commit is contained in:
anhefti 2024-06-05 08:44:46 +02:00
parent 17400023f3
commit db4411c52e
21 changed files with 674 additions and 406 deletions

View file

@ -37,7 +37,6 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.IndicatorDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.TransactionHandler;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
@Lazy
@Component
@ -53,7 +52,6 @@ public class DeleteExamAction implements BatchActionExec {
private final IndicatorDAO indicatorDAO;
private final AuthorizationService authorization;
private final UserActivityLogDAO userActivityLogDAO;
private final ExamSessionService examSessionService;
public DeleteExamAction(
final ExamDAO examDAO,
@ -62,8 +60,7 @@ public class DeleteExamAction implements BatchActionExec {
final ClientGroupDAO clientGroupDAO,
final IndicatorDAO indicatorDAO,
final AuthorizationService authorization,
final UserActivityLogDAO userActivityLogDAO,
final ExamSessionService examSessionService) {
final UserActivityLogDAO userActivityLogDAO) {
this.examDAO = examDAO;
this.clientConnectionDAO = clientConnectionDAO;
@ -72,7 +69,6 @@ public class DeleteExamAction implements BatchActionExec {
this.indicatorDAO = indicatorDAO;
this.authorization = authorization;
this.userActivityLogDAO = userActivityLogDAO;
this.examSessionService = examSessionService;
}
@Override
@ -149,13 +145,23 @@ public class DeleteExamAction implements BatchActionExec {
}
private Result<Exam> checkNoActiveSEBClientConnections(final Exam exam) {
if (this.examSessionService.hasActiveSEBClientConnections(exam.id)) {
return Result.ofError(new APIMessageException(
APIMessage.ErrorMessage.INTEGRITY_VALIDATION
.of("Exam currently has active SEB Client connections.")));
if (exam.status != Exam.ExamStatus.RUNNING) {
return Result.of(exam);
}
return Result.of(exam);
final Integer active = this.clientConnectionDAO
.getAllActiveConnectionTokens(exam.id)
.map(Collection::size)
.onError(error -> log.warn("Failed to get active access tokens for exam: {}", error.getMessage()))
.getOr(1);
if (active == null || active == 0) {
return Result.of(exam);
}
return Result.ofError(new APIMessageException(
APIMessage.ErrorMessage.INTEGRITY_VALIDATION
.of("Exam currently has active SEB Client connections.")));
}
private Result<Exam> logDeleted(final Exam entity, final BatchAction batchAction) {

View file

@ -48,7 +48,6 @@ public class ExamAdminServiceImpl implements ExamAdminService {
private final ConfigurationNodeDAO configurationNodeDAO;
private final ExamConfigurationMapDAO examConfigurationMapDAO;
private final LmsAPIService lmsAPIService;
private final ExamConfigurationValueService examConfigurationValueService;
private final SEBRestrictionService sebRestrictionService;
@ -248,26 +247,9 @@ public class ExamAdminServiceImpl implements ExamAdminService {
@Override
public Result<Exam> applyQuitPassword(final Exam exam) {
return this.examConfigurationValueService
.applyQuitPasswordToConfigs(exam.id, exam.quitPassword)
.map(id -> applySEBRestrictionIfExamRunning(exam))
.onError(t -> log.error("Failed to quit password for Exam: {}", exam, t));
return this.sebRestrictionService.applyQuitPassword(exam);
}
private Exam applySEBRestrictionIfExamRunning(final Exam exam) {
if (exam.status != ExamStatus.RUNNING) {
return exam;
}
return this.sebRestrictionService
.applySEBClientRestriction(exam)
.flatMap(e -> this.examDAO.setSEBRestriction(e.id, true))
.onError(t -> log.error("Failed to update SEB Client restriction for Exam: {}", exam, t))
.getOr(exam);
}
@Override
public Result<Exam> archiveExam(final Exam exam) {
return Result.tryCatch(() -> {

View file

@ -27,10 +27,8 @@ import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.AdditionalAttributesDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamAdminService;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamImportService;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamTemplateService;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.FullLmsIntegrationService;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionService;
@ -51,16 +49,15 @@ public class ExamImportServiceImpl implements ExamImportService {
private static final Logger log = LoggerFactory.getLogger(ExamImportServiceImpl.class);
private final ExamDAO examDAO;
// private final FullLmsIntegrationService fullLmsIntegrationService;
private final ExamTemplateService examTemplateService;
private final ExamAdminService examAdminService;
private final SEBRestrictionService sebRestrictionService;
private final boolean appSignatureKeyEnabled;
private final int defaultNumericalTrustThreshold;
public ExamImportServiceImpl(
final ExamDAO examDAO,
final ExamTemplateService examTemplateService,
final ExamAdminService examAdminService,
final SEBRestrictionService sebRestrictionService,
final AdditionalAttributesDAO additionalAttributesDAO,
final LmsAPIService lmsAPIService,
final @Value("${sebserver.webservice.api.admin.exam.app.signature.key.enabled:false}") boolean appSignatureKeyEnabled,
@ -68,7 +65,7 @@ public class ExamImportServiceImpl implements ExamImportService {
this.examDAO = examDAO;
this.examTemplateService = examTemplateService;
this.examAdminService = examAdminService;
this.sebRestrictionService = sebRestrictionService;
this.additionalAttributesDAO = additionalAttributesDAO;
this.lmsAPIService = lmsAPIService;
this.appSignatureKeyEnabled = appSignatureKeyEnabled;
@ -102,7 +99,7 @@ public class ExamImportServiceImpl implements ExamImportService {
})
.flatMap(this::applyAdditionalSEBRestrictions)
.onError(error -> errors.add(APIMessage.ErrorMessage.EXAM_IMPORT_ERROR_AUTO_RESTRICTION.of(error)))
.flatMap(examAdminService::applyQuitPassword)
.flatMap(sebRestrictionService::applyQuitPassword)
.onError(error -> errors.add(APIMessage.ErrorMessage.EXAM_IMPORT_ERROR_QUIT_PASSWORD.of(error)))
.flatMap(examTemplateService::applyScreenProctoringSettingsForExam)
.onError(error -> errors.add(APIMessage.ErrorMessage.EXAM_IMPORT_ERROR_SCREEN_PROCTORING_SETTINGS.of(error)));

View file

@ -72,18 +72,7 @@ public interface LmsAPIService {
* @return LmsAPITemplate for specified LmsSetup configuration */
Result<LmsAPITemplate> getLmsAPITemplate(String lmsSetupId);
/** use this to the specified LmsAPITemplate.
*
* @param template the LmsAPITemplate
* @return LmsSetupTestResult containing list of errors if happened */
LmsSetupTestResult test(LmsAPITemplate template);
/** This can be used to test an LmsSetup connection parameter without saving or heaving
* an already persistent version of an LmsSetup.
*
* @param lmsSetup the LmsSetup instance
* @return LmsSetupTestResult containing list of errors if happened */
LmsSetupTestResult testAdHoc(LmsSetup lmsSetup);
/** Get a LmsAPITemplate for specified LmsSetup configuration by primary key
*

View file

@ -0,0 +1,39 @@
/*
* Copyright (c) 2019 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.webservice.servicelayer.lms;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
public interface LmsAPITemplateCacheService {
/** Get a LmsAPITemplate for specified LmsSetup configuration by model identifier.
* Get it from cache if available of not, create new one and put it in cache if okay.
*
* @param lmsSetupId the identifier of LmsSetup
* @return LmsAPITemplate for specified LmsSetup configuration */
Result<LmsAPITemplate> getLmsAPITemplate(Long lmsSetupId);
/** Get a LmsAPITemplate for specified LmsSetup configuration by model identifier.
* Get it from cache if available of not, create new one and put it in cache if okay.
*
* @param lmsSetupId the identifier of LmsSetup
* @return LmsAPITemplate for specified LmsSetup configuration */
Result<LmsAPITemplate> getLmsAPITemplate(String lmsSetupId);
Result<LmsAPITemplate> getLmsAPITemplateForTesting(String lmsSetupId);
void clearCache(String lmsSetupId);
/** Reset and cleanup the caches if there are some */
void cleanup();
}

View file

@ -0,0 +1,28 @@
/*
* Copyright (c) 2019 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.webservice.servicelayer.lms;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
public interface LmsTestService {
/** use this to the specified LmsAPITemplate.
*
* @param template the LmsAPITemplate
* @return LmsSetupTestResult containing list of errors if happened */
LmsSetupTestResult test(LmsAPITemplate template);
/** This can be used to test an LmsSetup connection parameter without saving or heaving
* an already persistent version of an LmsSetup.
*
* @param lmsSetup the LmsSetup instance
* @return LmsSetupTestResult containing list of errors if happened */
LmsSetupTestResult testAdHoc(LmsSetup lmsSetup);
}

View file

@ -31,7 +31,7 @@ public interface SEBRestrictionService {
Result<SEBRestriction> getSEBRestrictionFromExam(Exam exam);
/** Saves the given SEBRestriction for the given Exam.
*
* <p>
* The webservice saves the given browser Exam keys within the Exam record
* and given additional restriction properties within the Additional attributes linked
* to the given Exam.
@ -65,4 +65,6 @@ public interface SEBRestrictionService {
* to the LMS */
boolean checkSebRestrictionSet(Exam exam);
Result<Exam> applyQuitPassword(final Exam exam);
}

View file

@ -44,8 +44,8 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamConfigurationValue
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamImportService;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamTemplateChangeEvent;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.FullLmsIntegrationService;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplateCacheService;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ConnectionConfigurationChangeEvent;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ConnectionConfigurationService;
@ -76,9 +76,9 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
private final SEBClientConfigDAO sebClientConfigDAO;
private final ConnectionConfigurationService connectionConfigurationService;
private final DeleteExamAction deleteExamAction;
private final LmsAPIService lmsAPIService;
private final LmsAPITemplateCacheService lmsAPITemplateCacheService;
private final ExamImportService examImportService;
private final ExamSessionService examSessionService;
private final ClientConnectionDAO clientConnectionDAO;
private final ExamConfigurationValueService examConfigurationValueService;
private final ExamDAO examDAO;
private final ExamTemplateDAO examTemplateDAO;
@ -96,16 +96,16 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
final ScreenProctoringService screenProctoringService,
final ConnectionConfigurationService connectionConfigurationService,
final DeleteExamAction deleteExamAction,
final LmsAPIService lmsAPIService,
final ExamSessionService examSessionService,
final ExamConfigurationValueService examConfigurationValueService,
final ExamDAO examDAO,
final ClientConnectionDAO clientConnectionDAO,
final ExamImportService examImportService,
final ExamTemplateDAO examTemplateDAO,
final WebserviceInfo webserviceInfo,
final ClientHttpRequestFactoryService clientHttpRequestFactoryService,
final UserService userService,
final TeacherAccountServiceImpl teacherAccountServiceImpl,
final LmsAPITemplateCacheService lmsAPITemplateCacheService,
@Value("${sebserver.webservice.lms.api.endpoint}") final String lmsAPIEndpoint,
@Value("${sebserver.webservice.lms.api.clientId}") final String clientId,
@Value("${sebserver.webservice.api.admin.clientSecret}") final String clientSecret) {
@ -116,8 +116,7 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
this.sebClientConfigDAO = sebClientConfigDAO;
this.connectionConfigurationService = connectionConfigurationService;
this.deleteExamAction = deleteExamAction;
this.lmsAPIService = lmsAPIService;
this.examSessionService = examSessionService;
this.lmsAPITemplateCacheService = lmsAPITemplateCacheService;
this.examDAO = examDAO;
this.examTemplateDAO = examTemplateDAO;
this.webserviceInfo = webserviceInfo;
@ -125,6 +124,7 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
this.userService = userService;
this.examConfigurationValueService = examConfigurationValueService;
this.examImportService = examImportService;
this.clientConnectionDAO = clientConnectionDAO;
resource = new ClientCredentialsResourceDetails();
resource.setAccessTokenUri(webserviceInfo.getOAuthTokenURI());
@ -246,7 +246,7 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
this.getIntegrationTemplates(lmsSetup.institutionId)
);
return lmsAPIService.getLmsAPITemplate(lmsSetupId)
return lmsAPITemplateCacheService.getLmsAPITemplate(lmsSetupId)
.getOrThrow()
.applyConnectionDetails(data)
.onError(error -> lmsSetupDAO
@ -271,7 +271,7 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
return false;
}
lmsAPIService.getLmsAPITemplate(lmsSetupId)
lmsAPITemplateCacheService.getLmsAPITemplate(lmsSetupId)
.getOrThrow()
.deleteConnectionDetails()
.onError(error -> lmsSetupDAO
@ -297,7 +297,7 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
return lmsSetupDAO
.getLmsSetupIdByConnectionId(lmsUUID)
.flatMap(lmsAPIService::getLmsAPITemplate)
.flatMap(lmsAPITemplateCacheService::getLmsAPITemplate)
.map(findQuizData(courseId, quizId))
.map(createExam(examTemplateId, quitPassword))
.map(exam -> applyExamData(exam, false))
@ -312,7 +312,7 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
return lmsSetupDAO
.getLmsSetupIdByConnectionId(lmsUUID)
.flatMap(lmsAPIService::getLmsAPITemplate)
.flatMap(lmsAPITemplateCacheService::getLmsAPITemplate)
.map(findQuizData(courseId, quizId))
.flatMap(this::findExam)
.map(this::checkDeletion)
@ -384,7 +384,7 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
return lmsSetupDAO
.getLmsSetupIdByConnectionId(lmsUUID)
.flatMap(lmsAPIService::getLmsAPITemplate)
.flatMap(lmsAPITemplateCacheService::getLmsAPITemplate)
.map(findQuizData(courseId, quizId))
.flatMap(this::findExam)
.flatMap(exam -> this.teacherAccountServiceImpl
@ -470,14 +470,32 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
private Exam checkDeletion(final Exam exam) {
// TODO check if Exam can be deleted according to the Spec
// check if there are no active SEB client connections
if (this.examSessionService.hasActiveSEBClientConnections(exam.id)) {
throw new APIMessage.APIMessageException(
APIMessage.ErrorMessage.INTEGRITY_VALIDATION
.of("Exam currently has active SEB Client connections."));
if (exam.status != Exam.ExamStatus.RUNNING) {
return exam;
}
return exam;
final Integer active = this.clientConnectionDAO
.getAllActiveConnectionTokens(exam.id)
.map(Collection::size)
.onError(error -> log.warn("Failed to get active access tokens for exam: {}", error.getMessage()))
.getOr(1);
if (active == null || active == 0) {
return exam;
}
throw new APIMessage.APIMessageException(
APIMessage.ErrorMessage.INTEGRITY_VALIDATION
.of("Exam currently has active SEB Client connections."));
// check if there are no active SEB client connections
// if (this.examSessionService.hasActiveSEBClientConnections(exam.id)) {
// throw new APIMessage.APIMessageException(
// APIMessage.ErrorMessage.INTEGRITY_VALIDATION
// .of("Exam currently has active SEB Client connections."));
// }
//
// return exam;
}
@ -497,7 +515,7 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
private Exam applyExamData(final Exam exam, final boolean deletion) {
try {
final LmsAPITemplate lmsAPITemplate = lmsAPIService
final LmsAPITemplate lmsAPITemplate = lmsAPITemplateCacheService
.getLmsAPITemplate(exam.lmsSetupId)
.getOrThrow();
final String lmsUUID = lmsAPITemplate.lmsSetup().connectionId;
@ -526,7 +544,7 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
}
private Exam applyConnectionConfiguration(final Exam exam) {
return lmsAPIService
return lmsAPITemplateCacheService
.getLmsAPITemplate(exam.lmsSetupId)
.flatMap(template -> {
final String connectionConfigId = getConnectionConfigurationId(exam);

View file

@ -8,36 +8,20 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.stream.Collectors;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.*;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.client.ClientCredentialService;
import ch.ethz.seb.sebserver.gbl.client.ClientCredentials;
import ch.ethz.seb.sebserver.gbl.client.ProxyData;
import ch.ethz.seb.sebserver.gbl.model.Page;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult.ErrorType;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.WebserviceInfo;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.LmsSetupDAO;
@ -48,32 +32,20 @@ public class LmsAPIServiceImpl implements LmsAPIService {
private static final Logger log = LoggerFactory.getLogger(LmsAPIServiceImpl.class);
private final WebserviceInfo webserviceInfo;
private final LmsSetupDAO lmsSetupDAO;
private final ClientCredentialService clientCredentialService;
private final QuizLookupService quizLookupService;
private final EnumMap<LmsType, LmsAPITemplateFactory> templateFactories;
private final Map<CacheKey, LmsAPITemplate> cache = new ConcurrentHashMap<>();
private final LmsAPITemplateCacheService lmsAPITemplateCacheService;
public LmsAPIServiceImpl(
final WebserviceInfo webserviceInfo,
final LmsSetupDAO lmsSetupDAO,
final ClientCredentialService clientCredentialService,
final QuizLookupService quizLookupService,
final Collection<LmsAPITemplateFactory> lmsAPITemplateFactories) {
final LmsAPITemplateCacheService lmsAPITemplateCacheService,
final Collection<LmsAPITemplateFactory> lmsAPITemplateFactories
) {
this.webserviceInfo = webserviceInfo;
this.lmsSetupDAO = lmsSetupDAO;
this.clientCredentialService = clientCredentialService;
this.quizLookupService = quizLookupService;
final Map<LmsType, LmsAPITemplateFactory> factories = lmsAPITemplateFactories
.stream()
.collect(Collectors.toMap(
LmsAPITemplateFactory::lmsType,
Function.identity()));
this.templateFactories = new EnumMap<>(factories);
this.lmsAPITemplateCacheService = lmsAPITemplateCacheService;
}
/** Listen to LmsSetupChangeEvent to release an affected LmsAPITemplate from cache
@ -90,19 +62,13 @@ public class LmsAPIServiceImpl implements LmsAPIService {
log.debug("LmsSetup changed. Update cache by removing eventually used references");
}
final LmsAPITemplate removedTemplate = this.cache.remove(
new CacheKey(lmsSetup.getModelId(), 0));
if (removedTemplate != null) {
removedTemplate.clearCourseCache();
}
lmsAPITemplateCacheService.clearCache(lmsSetup.getModelId());
this.quizLookupService.clear(lmsSetup.institutionId);
}
@Override
public void cleanup() {
this.cache.values().forEach(LmsAPITemplate::dispose);
this.cache.clear();
lmsAPITemplateCacheService.cleanup();
}
@Override
@ -127,258 +93,11 @@ public class LmsAPIServiceImpl implements LmsAPIService {
@Override
public Result<LmsAPITemplate> getLmsAPITemplate(final String lmsSetupId) {
return Result.tryCatch(() -> {
synchronized (this) {
final LmsAPITemplate lmsAPITemplate = getFromCache(lmsSetupId);
if (lmsAPITemplate == null) {
return createLmsSetupTemplate(lmsSetupId)
.onError(error -> log.error("Failed to create LMSSetup: ", error))
.onSuccess(t -> this.cache.put(new CacheKey(lmsSetupId, System.currentTimeMillis()), t))
.getOrThrow();
}
return lmsAPITemplate;
}
});
return lmsAPITemplateCacheService.getLmsAPITemplate(lmsSetupId);
}
@Override
public LmsSetupTestResult test(final LmsAPITemplate template) {
final LmsSetupTestResult testCourseAccessAPI = template.testCourseAccessAPI();
if (!testCourseAccessAPI.isOk()) {
this.cache.remove(new CacheKey(template.lmsSetup().getModelId(), 0));
return testCourseAccessAPI;
}
if (template.lmsSetup().getLmsType().features.contains(LmsSetup.Features.SEB_RESTRICTION)) {
final LmsSetupTestResult lmsSetupTestResult = template.testCourseRestrictionAPI();
if (!lmsSetupTestResult.isOk()) {
this.cache.remove(new CacheKey(template.lmsSetup().getModelId(), 0));
return lmsSetupTestResult;
}
}
if (template.lmsSetup().getLmsType().features.contains(LmsSetup.Features.LMS_FULL_INTEGRATION)) {
final Long lmsSetupId = template.lmsSetup().id;
final LmsSetupTestResult lmsSetupTestResult = template.testFullIntegrationAPI();
if (!lmsSetupTestResult.isOk()) {
this.cache.remove(new CacheKey(template.lmsSetup().getModelId(), 0));
this.lmsSetupDAO
.setIntegrationActive(lmsSetupId, false)
.onError(er -> log.error("Failed to mark LMS integration inactive", er));
return lmsSetupTestResult;
} else {
// TODO
// final Result<FullLmsIntegrationService.IntegrationData> integrationDataResult = fullLmsIntegrationService
// .applyFullLmsIntegration(template.lmsSetup().id);
//
// if (integrationDataResult.hasError()) {
// return LmsSetupTestResult.ofFullIntegrationAPIError(
// template.lmsSetup().lmsType,
// "Failed to apply full LMS integration");
// }
}
}
return LmsSetupTestResult.ofOkay(template.lmsSetup().getLmsType());
}
@Override
public LmsSetupTestResult testAdHoc(final LmsSetup lmsSetup) {
final AdHocAPITemplateDataSupplier apiTemplateDataSupplier = new AdHocAPITemplateDataSupplier(
lmsSetup,
this.clientCredentialService);
final Result<LmsAPITemplate> createLmsSetupTemplate = createLmsSetupTemplate(apiTemplateDataSupplier);
if (createLmsSetupTemplate.hasError()) {
return new LmsSetupTestResult(
lmsSetup.lmsType,
new LmsSetupTestResult.Error(ErrorType.TEMPLATE_CREATION,
createLmsSetupTemplate.getError().getMessage()));
}
final LmsAPITemplate lmsSetupTemplate = createLmsSetupTemplate.get();
final LmsSetupTestResult testCourseAccessAPI = lmsSetupTemplate.testCourseAccessAPI();
if (!testCourseAccessAPI.isOk()) {
return testCourseAccessAPI;
}
final LmsType lmsType = lmsSetupTemplate.lmsSetup().getLmsType();
if (lmsType.features.contains(LmsSetup.Features.SEB_RESTRICTION)) {
final LmsSetupTestResult lmsSetupTestResult = lmsSetupTemplate.testCourseRestrictionAPI();
if (!lmsSetupTestResult.isOk()) {
return lmsSetupTestResult;
}
}
if (lmsType.features.contains(LmsSetup.Features.LMS_FULL_INTEGRATION)) {
final LmsSetupTestResult lmsSetupTestResult = lmsSetupTemplate.testFullIntegrationAPI();
if (!lmsSetupTestResult.isOk()) {
return lmsSetupTestResult;
}
}
return LmsSetupTestResult.ofOkay(lmsSetupTemplate.lmsSetup().getLmsType());
}
private LmsAPITemplate getFromCache(final String lmsSetupId) {
// first cleanup the cache by removing old instances
final long currentTimeMillis = System.currentTimeMillis();
new ArrayList<>(this.cache.keySet())
.stream()
.filter(key -> key.creationTimestamp - currentTimeMillis > Constants.DAY_IN_MILLIS)
.forEach(this.cache::remove);
// get from cache
final CacheKey cacheKey = new CacheKey(lmsSetupId, 0);
final LmsAPITemplate lmsAPITemplate = this.cache.get(cacheKey);
// in distributed setup, check if lmsSetup is up to date
if (this.webserviceInfo.isDistributed()) {
if (lmsAPITemplate == null) {
return null;
}
final LmsSetup lmsSetup = lmsAPITemplate.lmsSetup();
if (!this.lmsSetupDAO.isUpToDate(lmsSetup)) {
this.cache.remove(cacheKey);
this.quizLookupService.clear(lmsSetup.institutionId);
return null;
}
}
return lmsAPITemplate;
}
private Result<LmsAPITemplate> createLmsSetupTemplate(final String lmsSetupId) {
if (log.isDebugEnabled()) {
log.debug("Create new LmsAPITemplate for id: {}", lmsSetupId);
}
return createLmsSetupTemplate(new PersistentAPITemplateDataSupplier(
lmsSetupId,
this.lmsSetupDAO));
}
private Result<LmsAPITemplate> createLmsSetupTemplate(final APITemplateDataSupplier apiTemplateDataSupplier) {
final LmsType lmsType = apiTemplateDataSupplier.getLmsSetup().lmsType;
if (!this.templateFactories.containsKey(lmsType)) {
throw new UnsupportedOperationException("No support for LMS Type: " + lmsType);
}
final LmsAPITemplateFactory lmsAPITemplateFactory = this.templateFactories
.get(lmsType);
return lmsAPITemplateFactory
.create(apiTemplateDataSupplier);
}
/** Used to always get the actual LMS connection data from persistent */
private static final class PersistentAPITemplateDataSupplier implements APITemplateDataSupplier {
private final String lmsSetupId;
private final LmsSetupDAO lmsSetupDAO;
public PersistentAPITemplateDataSupplier(final String lmsSetupId, final LmsSetupDAO lmsSetupDAO) {
this.lmsSetupId = lmsSetupId;
this.lmsSetupDAO = lmsSetupDAO;
}
@Override
public LmsSetup getLmsSetup() {
return this.lmsSetupDAO.byModelId(this.lmsSetupId).getOrThrow();
}
@Override
public ClientCredentials getLmsClientCredentials() {
return this.lmsSetupDAO.getLmsAPIAccessCredentials(this.lmsSetupId).getOrThrow();
}
@Override
public ProxyData getProxyData() {
return this.lmsSetupDAO.getLmsAPIAccessProxyData(this.lmsSetupId).getOr(null);
}
}
/** Used to test LMS connection data that are not yet persistently stored */
private static final class AdHocAPITemplateDataSupplier implements APITemplateDataSupplier {
private final LmsSetup lmsSetup;
private final ClientCredentialService clientCredentialService;
public AdHocAPITemplateDataSupplier(
final LmsSetup lmsSetup,
final ClientCredentialService clientCredentialService) {
this.lmsSetup = lmsSetup;
this.clientCredentialService = clientCredentialService;
}
@Override
public LmsSetup getLmsSetup() {
return this.lmsSetup;
}
@Override
public ClientCredentials getLmsClientCredentials() {
return this.clientCredentialService.encryptClientCredentials(
this.lmsSetup.getLmsAuthName(),
this.lmsSetup.getLmsAuthSecret(),
this.lmsSetup.lmsRestApiToken)
.getOrThrow();
}
@Override
public ProxyData getProxyData() {
return (StringUtils.isNoneBlank(this.lmsSetup.proxyHost))
? new ProxyData(
this.lmsSetup.proxyHost,
this.lmsSetup.proxyPort,
this.clientCredentialService.encryptClientCredentials(
this.lmsSetup.proxyAuthUsername,
this.lmsSetup.proxyAuthSecret,
this.lmsSetup.lmsRestApiToken)
.getOrThrow())
: null;
}
}
private static final class CacheKey {
final String lmsSetupId;
final long creationTimestamp;
final int hash;
CacheKey(final String lmsSetupId, final long creationTimestamp) {
this.lmsSetupId = lmsSetupId;
this.creationTimestamp = creationTimestamp;
final int prime = 31;
int result = 1;
result = prime * result + ((lmsSetupId == null) ? 0 : lmsSetupId.hashCode());
this.hash = result;
}
@Override
public int hashCode() {
return this.hash;
}
@Override
public boolean equals(final Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
final CacheKey other = (CacheKey) obj;
if (this.lmsSetupId == null) {
if (other.lmsSetupId != null)
return false;
} else if (!this.lmsSetupId.equals(other.lmsSetupId))
return false;
return true;
}
}
}

View file

@ -0,0 +1,275 @@
/*
* Copyright (c) 2019 ETH Zürich, IT Services
*
* 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.ArrayList;
import java.util.Collection;
import java.util.EnumMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.stream.Collectors;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.client.ClientCredentialService;
import ch.ethz.seb.sebserver.gbl.client.ClientCredentials;
import ch.ethz.seb.sebserver.gbl.client.ProxyData;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.WebserviceInfo;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.LmsSetupDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.*;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
@Lazy
@Service
@WebServiceProfile
public class LmsAPITemplateCacheServiceImpl implements LmsAPITemplateCacheService {
private static final Logger log = LoggerFactory.getLogger(LmsAPITemplateCacheServiceImpl.class);
private final WebserviceInfo webserviceInfo;
private final LmsSetupDAO lmsSetupDAO;
private final QuizLookupService quizLookupService;
private final ClientCredentialService clientCredentialService;
private final EnumMap<LmsSetup.LmsType, LmsAPITemplateFactory> templateFactories;
private final Map<CacheKey, LmsAPITemplate> cache = new ConcurrentHashMap<>();
public LmsAPITemplateCacheServiceImpl(
final WebserviceInfo webserviceInfo,
final LmsSetupDAO lmsSetupDAO,
final QuizLookupService quizLookupService,
final ClientCredentialService clientCredentialService,
final Collection<LmsAPITemplateFactory> lmsAPITemplateFactories) {
this.webserviceInfo = webserviceInfo;
this.lmsSetupDAO = lmsSetupDAO;
this.quizLookupService = quizLookupService;
this.clientCredentialService = clientCredentialService;
final Map<LmsSetup.LmsType, LmsAPITemplateFactory> factories = lmsAPITemplateFactories
.stream()
.collect(Collectors.toMap(
LmsAPITemplateFactory::lmsType,
Function.identity()));
this.templateFactories = new EnumMap<>(factories);
}
@Override
public void cleanup() {
this.cache.values().forEach(LmsAPITemplate::dispose);
this.cache.clear();
}
@Override
public Result<LmsAPITemplate> getLmsAPITemplate(final Long lmsSetupId) {
return getLmsAPITemplate(String.valueOf(lmsSetupId));
}
@Override
public Result<LmsAPITemplate> getLmsAPITemplate(final String lmsSetupId) {
return Result.tryCatch(() -> {
synchronized (this) {
final LmsAPITemplate lmsAPITemplate = getFromCache(lmsSetupId);
if (lmsAPITemplate == null) {
return createLmsSetupTemplate(lmsSetupId)
.onError(error -> log.error("Failed to create LMSSetup: ", error))
.onSuccess(t -> this.cache.put(new CacheKey(lmsSetupId, System.currentTimeMillis()), t))
.getOrThrow();
}
return lmsAPITemplate;
}
});
}
@Override
public Result<LmsAPITemplate> getLmsAPITemplateForTesting(final String lmsSetupId) {
return lmsSetupDAO.byModelId(lmsSetupId)
.map(lmsSetup -> new AdHocAPITemplateDataSupplier(
lmsSetup,
this.clientCredentialService))
.flatMap(this::createLmsSetupTemplate);
}
@Override
public void clearCache(final String lmsSetupId) {
final LmsAPITemplate removedTemplate = this.cache.remove(
new CacheKey(lmsSetupId, 0));
if (removedTemplate != null) {
log.info("Removed LmsAPITemplate from cache: {}", removedTemplate);
removedTemplate.clearCourseCache();
}
}
private LmsAPITemplate getFromCache(final String lmsSetupId) {
// first cleanup the cache by removing old instances
final long currentTimeMillis = System.currentTimeMillis();
new ArrayList<>(this.cache.keySet())
.stream()
.filter(key -> key.creationTimestamp - currentTimeMillis > Constants.DAY_IN_MILLIS)
.forEach(this.cache::remove);
// get from cache
final CacheKey cacheKey = new CacheKey(lmsSetupId, 0);
final LmsAPITemplate lmsAPITemplate = this.cache.get(cacheKey);
// in distributed setup, check if lmsSetup is up to date
if (this.webserviceInfo.isDistributed()) {
if (lmsAPITemplate == null) {
return null;
}
final LmsSetup lmsSetup = lmsAPITemplate.lmsSetup();
if (!this.lmsSetupDAO.isUpToDate(lmsSetup)) {
this.cache.remove(cacheKey);
this.quizLookupService.clear(lmsSetup.institutionId);
return null;
}
}
return lmsAPITemplate;
}
private Result<LmsAPITemplate> createLmsSetupTemplate(final String lmsSetupId) {
if (log.isDebugEnabled()) {
log.debug("Create new LmsAPITemplate for id: {}", lmsSetupId);
}
return createLmsSetupTemplate(new PersistentAPITemplateDataSupplier(
lmsSetupId,
this.lmsSetupDAO));
}
private Result<LmsAPITemplate> createLmsSetupTemplate(final APITemplateDataSupplier apiTemplateDataSupplier) {
final LmsSetup.LmsType lmsType = apiTemplateDataSupplier.getLmsSetup().lmsType;
if (!this.templateFactories.containsKey(lmsType)) {
throw new UnsupportedOperationException("No support for LMS Type: " + lmsType);
}
final LmsAPITemplateFactory lmsAPITemplateFactory = this.templateFactories
.get(lmsType);
return lmsAPITemplateFactory
.create(apiTemplateDataSupplier);
}
/** Used to always get the actual LMS connection data from persistent */
private static final class PersistentAPITemplateDataSupplier implements APITemplateDataSupplier {
private final String lmsSetupId;
private final LmsSetupDAO lmsSetupDAO;
public PersistentAPITemplateDataSupplier(final String lmsSetupId, final LmsSetupDAO lmsSetupDAO) {
this.lmsSetupId = lmsSetupId;
this.lmsSetupDAO = lmsSetupDAO;
}
@Override
public LmsSetup getLmsSetup() {
return this.lmsSetupDAO.byModelId(this.lmsSetupId).getOrThrow();
}
@Override
public ClientCredentials getLmsClientCredentials() {
return this.lmsSetupDAO.getLmsAPIAccessCredentials(this.lmsSetupId).getOrThrow();
}
@Override
public ProxyData getProxyData() {
return this.lmsSetupDAO.getLmsAPIAccessProxyData(this.lmsSetupId).getOr(null);
}
}
/** Used to test LMS connection data that are not yet persistently stored */
private static final class AdHocAPITemplateDataSupplier implements APITemplateDataSupplier {
private final LmsSetup lmsSetup;
private final ClientCredentialService clientCredentialService;
public AdHocAPITemplateDataSupplier(
final LmsSetup lmsSetup,
final ClientCredentialService clientCredentialService) {
this.lmsSetup = lmsSetup;
this.clientCredentialService = clientCredentialService;
}
@Override
public LmsSetup getLmsSetup() {
return this.lmsSetup;
}
@Override
public ClientCredentials getLmsClientCredentials() {
return this.clientCredentialService.encryptClientCredentials(
this.lmsSetup.getLmsAuthName(),
this.lmsSetup.getLmsAuthSecret(),
this.lmsSetup.lmsRestApiToken)
.getOrThrow();
}
@Override
public ProxyData getProxyData() {
return (StringUtils.isNoneBlank(this.lmsSetup.proxyHost))
? new ProxyData(
this.lmsSetup.proxyHost,
this.lmsSetup.proxyPort,
this.clientCredentialService.encryptClientCredentials(
this.lmsSetup.proxyAuthUsername,
this.lmsSetup.proxyAuthSecret,
this.lmsSetup.lmsRestApiToken)
.getOrThrow())
: null;
}
}
private static final class CacheKey {
final String lmsSetupId;
final long creationTimestamp;
final int hash;
CacheKey(final String lmsSetupId, final long creationTimestamp) {
this.lmsSetupId = lmsSetupId;
this.creationTimestamp = creationTimestamp;
final int prime = 31;
int result = 1;
result = prime * result + ((lmsSetupId == null) ? 0 : lmsSetupId.hashCode());
this.hash = result;
}
@Override
public int hashCode() {
return this.hash;
}
@Override
public boolean equals(final Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
final CacheKey other = (CacheKey) obj;
if (this.lmsSetupId == null) {
if (other.lmsSetupId != null)
return false;
} else if (!this.lmsSetupId.equals(other.lmsSetupId))
return false;
return true;
}
}
}

View file

@ -0,0 +1,122 @@
/*
* Copyright (c) 2019 ETH Zürich, IT Services
*
* 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 ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.LmsSetupDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.FullLmsIntegrationService;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplateCacheService;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsTestService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
@Lazy
@Service
@WebServiceProfile
public class LmsTestServiceImpl implements LmsTestService {
private static final Logger log = LoggerFactory.getLogger(LmsTestServiceImpl.class);
private final LmsSetupDAO lmsSetupDAO;
private final LmsAPITemplateCacheService lmsAPITemplateCacheService;
private final FullLmsIntegrationService fullLmsIntegrationService;
public LmsTestServiceImpl(
final LmsSetupDAO lmsSetupDAO,
final LmsAPITemplateCacheService lmsAPITemplateCacheService,
final FullLmsIntegrationService fullLmsIntegrationService) {
this.lmsSetupDAO = lmsSetupDAO;
this.lmsAPITemplateCacheService = lmsAPITemplateCacheService;
this.fullLmsIntegrationService = fullLmsIntegrationService;
}
@Override
public LmsSetupTestResult test(final LmsAPITemplate template) {
final LmsSetupTestResult testCourseAccessAPI = template.testCourseAccessAPI();
if (!testCourseAccessAPI.isOk()) {
lmsAPITemplateCacheService.clearCache(template.lmsSetup().getModelId());
return testCourseAccessAPI;
}
if (template.lmsSetup().getLmsType().features.contains(LmsSetup.Features.SEB_RESTRICTION)) {
final LmsSetupTestResult lmsSetupTestResult = template.testCourseRestrictionAPI();
if (!lmsSetupTestResult.isOk()) {
lmsAPITemplateCacheService.clearCache(template.lmsSetup().getModelId());
return lmsSetupTestResult;
}
}
if (template.lmsSetup().getLmsType().features.contains(LmsSetup.Features.LMS_FULL_INTEGRATION)) {
final Long lmsSetupId = template.lmsSetup().id;
final LmsSetupTestResult lmsSetupTestResult = template.testFullIntegrationAPI();
if (!lmsSetupTestResult.isOk()) {
lmsAPITemplateCacheService.clearCache(template.lmsSetup().getModelId());
this.lmsSetupDAO
.setIntegrationActive(lmsSetupId, false)
.onError(er -> log.error("Failed to mark LMS integration inactive", er));
return lmsSetupTestResult;
} else {
final Result<FullLmsIntegrationService.IntegrationData> integrationDataResult = fullLmsIntegrationService
.applyFullLmsIntegration(template.lmsSetup().id);
if (integrationDataResult.hasError()) {
return LmsSetupTestResult.ofFullIntegrationAPIError(
template.lmsSetup().lmsType,
"Failed to apply full LMS integration");
}
}
}
return LmsSetupTestResult.ofOkay(template.lmsSetup().getLmsType());
}
@Override
public LmsSetupTestResult testAdHoc(final LmsSetup lmsSetup) {
final Result<LmsAPITemplate> createLmsSetupTemplate = lmsAPITemplateCacheService
.getLmsAPITemplateForTesting(lmsSetup.getModelId());
if (createLmsSetupTemplate.hasError()) {
return new LmsSetupTestResult(
lmsSetup.lmsType,
new LmsSetupTestResult.Error(LmsSetupTestResult.ErrorType.TEMPLATE_CREATION,
createLmsSetupTemplate.getError().getMessage()));
}
final LmsAPITemplate lmsSetupTemplate = createLmsSetupTemplate.get();
final LmsSetupTestResult testCourseAccessAPI = lmsSetupTemplate.testCourseAccessAPI();
if (!testCourseAccessAPI.isOk()) {
return testCourseAccessAPI;
}
final LmsSetup.LmsType lmsType = lmsSetupTemplate.lmsSetup().getLmsType();
if (lmsType.features.contains(LmsSetup.Features.SEB_RESTRICTION)) {
final LmsSetupTestResult lmsSetupTestResult = lmsSetupTemplate.testCourseRestrictionAPI();
if (!lmsSetupTestResult.isOk()) {
return lmsSetupTestResult;
}
}
if (lmsType.features.contains(LmsSetup.Features.LMS_FULL_INTEGRATION)) {
final LmsSetupTestResult lmsSetupTestResult = lmsSetupTemplate.testFullIntegrationAPI();
if (!lmsSetupTestResult.isOk()) {
return lmsSetupTestResult;
}
}
return LmsSetupTestResult.ofOkay(lmsSetupTemplate.lmsSetup().getLmsType());
}
}

View file

@ -19,6 +19,7 @@ import java.util.Set;
import java.util.stream.Collectors;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.AdditionalAttributeRecord;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamConfigurationValueService;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -58,17 +59,20 @@ public class SEBRestrictionServiceImpl implements SEBRestrictionService {
private final LmsAPIService lmsAPIService;
private final AdditionalAttributesDAO additionalAttributesDAO;
private final ExamConfigService examConfigService;
private final ExamConfigurationValueService examConfigurationValueService;
protected SEBRestrictionServiceImpl(
final ExamDAO examDAO,
final LmsAPIService lmsAPIService,
final AdditionalAttributesDAO additionalAttributesDAO,
final ExamConfigService examConfigService) {
final ExamConfigService examConfigService,
final ExamConfigurationValueService examConfigurationValueService) {
this.examDAO = examDAO;
this.lmsAPIService = lmsAPIService;
this.additionalAttributesDAO = additionalAttributesDAO;
this.examConfigService = examConfigService;
this.examConfigurationValueService = examConfigurationValueService;
}
@Override
@ -94,6 +98,25 @@ public class SEBRestrictionServiceImpl implements SEBRestrictionService {
return true;
}
@Override
public Result<Exam> applyQuitPassword(final Exam exam) {
return this.examConfigurationValueService
.applyQuitPasswordToConfigs(exam.id, exam.quitPassword)
.map(id -> applySEBRestrictionIfExamRunning(exam))
.onError(t -> log.error("Failed to quit password for Exam: {}", exam, t));
}
private Exam applySEBRestrictionIfExamRunning(final Exam exam) {
if (exam.status != Exam.ExamStatus.RUNNING) {
return exam;
}
return this.applySEBClientRestriction(exam)
.flatMap(e -> this.examDAO.setSEBRestriction(e.id, true))
.onError(t -> log.error("Failed to update SEB Client restriction for Exam: {}", exam, t))
.getOr(exam);
}
@Override
@Transactional
public Result<SEBRestriction> getSEBRestrictionFromExam(final Exam exam) {

View file

@ -8,6 +8,8 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle;
import java.util.Map;
import org.springframework.util.MultiValueMap;
import com.fasterxml.jackson.annotation.JsonCreator;
@ -37,7 +39,10 @@ public interface MoodleAPIRestTemplate {
String callMoodleAPIFunction(String functionName);
String postToMoodleAPIFunction(String functionName, String body);
String postToMoodleAPIFunction(
String functionName,
MultiValueMap<String, String> queryParams,
Map<String, Map<String, String>> queryAttributes);
String callMoodleAPIFunction(
String functionName,

View file

@ -320,7 +320,10 @@ public class MoodleRestTemplateFactoryImpl implements MoodleRestTemplateFactory
}
@Override
public String postToMoodleAPIFunction(final String functionName, final String body) {
public String postToMoodleAPIFunction(
final String functionName,
final MultiValueMap<String, String> queryParams,
final Map<String, Map<String, String>> queryAttributes) {
getAccessToken();
final UriComponentsBuilder queryParam = UriComponentsBuilder
@ -329,15 +332,35 @@ public class MoodleRestTemplateFactoryImpl implements MoodleRestTemplateFactory
.queryParam(REST_REQUEST_FUNCTION_NAME, functionName)
.queryParam(REST_REQUEST_FORMAT_NAME, "json");
if (queryParams != null && !queryParams.isEmpty()) {
queryParam.queryParams(queryParams);
}
final String body = createMoodleFormPostBody(queryAttributes);
final HttpHeaders headers = new HttpHeaders();
headers.set(
HttpHeaders.CONTENT_TYPE,
MediaType.APPLICATION_JSON_VALUE);
MediaType.APPLICATION_FORM_URLENCODED_VALUE);
final HttpEntity<String> httpEntity = new HttpEntity<>(body, headers);
return doRequest(functionName, queryParam, true, httpEntity);
}
private String createMoodleFormPostBody(final Map<String, Map<String, String>> queryAttributes) {
if (queryAttributes == null) {
return null;
}
final StringBuffer sb = new StringBuffer();
queryAttributes.forEach(
(name1, value1) -> value1.forEach(
(key, value) -> sb.append(name1).append("[").append(key).append("]=").append(value).append("&")));
sb.deleteCharAt(sb.length() - 1);
return sb.toString();
}
@Override
public String callMoodleAPIFunction(
final String functionName,

View file

@ -8,6 +8,9 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.plugin;
import java.util.HashMap;
import java.util.Map;
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
@ -21,6 +24,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleAPIRe
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleResponseException;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -35,6 +39,8 @@ public class MoodlePluginFullIntegration implements FullLmsIntegrationAPI {
private static final String FUNCTION_NAME_SEBSERVER_CONNECTION = "quizaccess_sebserver_connection";
private static final String FUNCTION_NAME_SEBSERVER_CONNECTION_DELETE = "quizaccess_sebserver_connection_delete";
private static final String FUNCTION_NAME_SET_EXAM_DATA = "quizaccess_sebserver_set_exam_data";
private static final String ATTRIBUTE_CONNECTION = "connection";
private static final String ATTRIBUTE_EXAM_DATA = "data";
private static final String UPLOAD_ENDPOINT = "/mod/quiz/accessrule/sebserver/uploadconfig.php";
@ -94,14 +100,20 @@ public class MoodlePluginFullIntegration implements FullLmsIntegrationAPI {
// apply
final LmsSetup lmsSetup = this.restTemplateFactory.getApiTemplateDataSupplier().getLmsSetup();
final String jsonPayload = jsonMapper.writeValueAsString(data);
final String connectionJSON = jsonMapper.writeValueAsString(data);
final MoodleAPIRestTemplate rest = getRestTemplate().getOrThrow();
//if (log.isDebugEnabled()) {
log.info("Try to connect to Moodle Plugin 2.0 with: {}", jsonPayload);
log.info("Try to connect to Moodle Plugin 2.0 with: {}", connectionJSON);
//}
final String response = rest.postToMoodleAPIFunction(FUNCTION_NAME_SEBSERVER_CONNECTION, jsonPayload);
final MultiValueMap<String, String> queryAttributes = new LinkedMultiValueMap<>();
queryAttributes.add(ATTRIBUTE_CONNECTION, connectionJSON);
final String response = rest.postToMoodleAPIFunction(
FUNCTION_NAME_SEBSERVER_CONNECTION,
queryAttributes,
null);
if (response != null && response.startsWith("{\"exception\":")) {
// Seems there was an error response from Moodle side.
@ -141,9 +153,31 @@ public class MoodlePluginFullIntegration implements FullLmsIntegrationAPI {
}
final LmsSetup lmsSetup = this.restTemplateFactory.getApiTemplateDataSupplier().getLmsSetup();
final String jsonPayload = jsonMapper.writeValueAsString(examData);
final MoodleAPIRestTemplate rest = getRestTemplate().getOrThrow();
final String response = rest.postToMoodleAPIFunction(FUNCTION_NAME_SET_EXAM_DATA, jsonPayload);
final Map<String, Map<String, String>> attributes = new HashMap<>();
final Map<String, String> data_mapping = new HashMap<>();
attributes.put(ATTRIBUTE_EXAM_DATA, data_mapping);
// data[quizid]= int
// data[addordelete]= int
// data[templateid]= int
// data[showquitlink]= int
// data[quitsecret]= string
data_mapping.put("quizid", examData.quiz_id);
if (BooleanUtils.isTrue(examData.exam_created)) {
data_mapping.put("addordelete", "1");
data_mapping.put("templateid", examData.template_id);
data_mapping.put("showquitlink", BooleanUtils.isTrue(examData.show_quit_link) ? "1" : "2");
data_mapping.put("quitsecret", examData.quit_password);
} else {
data_mapping.put("addordelete", "0");
}
final String response = rest.postToMoodleAPIFunction(
FUNCTION_NAME_SET_EXAM_DATA,
null,
attributes);
if (response != null && (response.startsWith("{\"exception\":") || response.startsWith("0"))) {
log.warn("Failed to apply Exam data to moodle: {}", examData);

View file

@ -126,14 +126,6 @@ public class ExamSessionCacheService {
case RUNNING: {
return true;
}
case UP_COMING:
case FINISHED: {
return false;
// TODO do we really need to double-check here?
// return this.examUpdateHandler.updateRunning(exam.id)
// .map(e -> e.status == ExamStatus.RUNNING)
// .getOr(false);
}
default: {
return false;
}

View file

@ -23,6 +23,8 @@ import java.util.stream.Collectors;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientConnectionDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.ExamSessionCacheService;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
@ -52,7 +54,6 @@ import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringRoomConnection;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringServerType;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction;
import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
@ -62,7 +63,6 @@ import ch.ethz.seb.sebserver.gbl.util.Tuple;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.WebserviceInfo;
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.AuthorizationService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.RemoteProctoringService;
@Lazy
@ -104,7 +104,8 @@ public class JitsiProctoringService implements RemoteProctoringService {
.stream().collect(Collectors.toMap(Tuple::get_1, Tuple::get_2)));
private final AuthorizationService authorizationService;
private final ExamSessionService examSessionService;
private final ClientConnectionDAO clientConnectionDAO;
private final ExamSessionCacheService examSessionCacheService;
private final Cryptor cryptor;
private final ClientHttpRequestFactoryService clientHttpRequestFactoryService;
private final JSONMapper jsonMapper;
@ -112,14 +113,16 @@ public class JitsiProctoringService implements RemoteProctoringService {
protected JitsiProctoringService(
final AuthorizationService authorizationService,
final ExamSessionService examSessionService,
final ClientConnectionDAO clientConnectionDAO,
final ExamSessionCacheService examSessionCacheService,
final Cryptor cryptor,
final ClientHttpRequestFactoryService clientHttpRequestFactoryService,
final JSONMapper jsonMapper,
final WebserviceInfo webserviceInfo) {
this.authorizationService = authorizationService;
this.examSessionService = examSessionService;
this.clientConnectionDAO = clientConnectionDAO;
this.examSessionCacheService = examSessionCacheService;
this.cryptor = cryptor;
this.clientHttpRequestFactoryService = clientHttpRequestFactoryService;
this.jsonMapper = jsonMapper;
@ -310,8 +313,9 @@ public class JitsiProctoringService implements RemoteProctoringService {
final String subject) {
return Result.tryCatch(() -> {
final ClientConnectionData clientConnection = this.examSessionService
.getConnectionData(connectionToken)
final ClientConnection clientConnection = clientConnectionDAO
.byConnectionToken(connectionToken)
.getOrThrow();
return createProctoringConnection(
@ -319,7 +323,7 @@ public class JitsiProctoringService implements RemoteProctoringService {
proctoringSettings.serverURL,
proctoringSettings.appKey,
proctoringSettings.getAppSecret(),
clientConnection.clientConnection.userSessionId,
clientConnection.userSessionId,
SEB_CLIENT_KEY,
roomName,
subject,
@ -476,13 +480,12 @@ public class JitsiProctoringService implements RemoteProctoringService {
}
long expTime = System.currentTimeMillis() + Constants.DAY_IN_MILLIS;
if (this.examSessionService.isExamRunning(examProctoring.examId)) {
final Exam exam = this.examSessionService.getRunningExam(examProctoring.examId)
.getOrThrow();
if (exam.endTime != null) {
expTime = exam.endTime.getMillis();
}
final Exam runningExam = examSessionCacheService.getRunningExam(examProctoring.examId);
if (runningExam != null && runningExam.endTime != null) {
expTime = runningExam.endTime.getMillis();
}
return expTime;
}

View file

@ -26,6 +26,8 @@ import java.util.stream.Collectors;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.ExamSessionCacheService;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.HttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
@ -134,7 +136,7 @@ public class ZoomProctoringService implements RemoteProctoringService {
Constants.FALSE_STRING))
.stream().collect(Collectors.toMap(Tuple::get_1, Tuple::get_2)));
private final ExamSessionService examSessionService;
private final ExamSessionCacheService examSessionCacheService;
private final Cryptor cryptor;
private final AsyncService asyncService;
private final JSONMapper jsonMapper;
@ -146,7 +148,7 @@ public class ZoomProctoringService implements RemoteProctoringService {
private final int tokenExpirySeconds;
public ZoomProctoringService(
final ExamSessionService examSessionService,
final ExamSessionCacheService examSessionCacheService,
final Cryptor cryptor,
final AsyncService asyncService,
final JSONMapper jsonMapper,
@ -157,7 +159,7 @@ public class ZoomProctoringService implements RemoteProctoringService {
@Value("${sebserver.webservice.proctoring.sendRejoinForCollectingRoom:false}") final boolean sendRejoinForCollectingRoom,
@Value("${sebserver.webservice.proctoring.zoom.tokenexpiry.seconds:86400}") final int tokenExpirySeconds) {
this.examSessionService = examSessionService;
this.examSessionCacheService = examSessionCacheService;
this.cryptor = cryptor;
this.asyncService = asyncService;
this.jsonMapper = jsonMapper;
@ -326,9 +328,7 @@ public class ZoomProctoringService implements RemoteProctoringService {
remoteProctoringRoom.additionalRoomData,
AdditionalZoomRoomData.class);
final ClientConnectionData clientConnection = this.examSessionService
.getConnectionData(connectionToken)
.getOrThrow();
final ClientConnectionData clientConnection = this.examSessionCacheService.getClientConnection(connectionToken);
// Note: since SEB Server version 1.5 we work only with SDKKey instead of AppKey which is deprecated
final ClientCredentials sdkCredentials = new ClientCredentials(
@ -354,7 +354,7 @@ public class ZoomProctoringService implements RemoteProctoringService {
sdkCredentials.accessToken,
sdkCredentials.clientId,
String.valueOf(additionalZoomRoomData.meeting_id),
clientConnection.clientConnection.userSessionId,
(clientConnection != null) ? clientConnection.clientConnection.userSessionId : "Unknown",
remoteProctoringRoom.additionalRoomData);
});
}
@ -405,10 +405,11 @@ public class ZoomProctoringService implements RemoteProctoringService {
private int getMeetingDuration(final Long examId) {
try {
final DateTime endTime = this.examSessionService
.getRunningExam(examId)
.getOrThrow()
.getEndTime();
;
final DateTime endTime = examSessionCacheService
.getRunningExam(examId).endTime;
final Long result = new Interval(DateTime.now(DateTimeZone.UTC), endTime)
.toDurationMillis() / Constants.MINUTE_IN_MILLIS;
return result.intValue();

View file

@ -10,6 +10,7 @@ package ch.ethz.seb.sebserver.webservice.weblayer.api;
import javax.validation.Valid;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsTestService;
import org.mybatis.dynamic.sql.SqlTable;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.http.MediaType;
@ -50,6 +51,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationSe
public class LmsSetupController extends ActivatableEntityController<LmsSetup, LmsSetup> {
private final LmsAPIService lmsAPIService;
private final LmsTestService lmsTestService;
final ApplicationEventPublisher applicationEventPublisher;
public LmsSetupController(
@ -60,6 +62,7 @@ public class LmsSetupController extends ActivatableEntityController<LmsSetup, Lm
final LmsAPIService lmsAPIService,
final PaginationService paginationService,
final BeanValidationService beanValidationService,
final LmsTestService lmsTestService,
final ApplicationEventPublisher applicationEventPublisher) {
super(authorization,
@ -70,6 +73,7 @@ public class LmsSetupController extends ActivatableEntityController<LmsSetup, Lm
beanValidationService);
this.lmsAPIService = lmsAPIService;
this.lmsTestService = lmsTestService;
this.applicationEventPublisher = applicationEventPublisher;
}
@ -97,7 +101,7 @@ public class LmsSetupController extends ActivatableEntityController<LmsSetup, Lm
final LmsSetupTestResult result = this.lmsAPIService
.getLmsAPITemplate(modelId)
.map(this.lmsAPIService::test)
.map(this.lmsTestService::test)
.onErrorDo(error -> {
final LmsType lmsType = this.entityDAO.byPK(modelId).get().lmsType;
return new LmsSetupTestResult(
@ -122,7 +126,7 @@ public class LmsSetupController extends ActivatableEntityController<LmsSetup, Lm
this.authorization.checkModify(lmsSetup);
final LmsSetupTestResult result = this.lmsAPIService.testAdHoc(lmsSetup);
final LmsSetupTestResult result = this.lmsTestService.testAdHoc(lmsSetup);
if (result.missingLMSSetupAttribute != null && !result.missingLMSSetupAttribute.isEmpty()) {
throw new APIMessageException(result.missingLMSSetupAttribute);
}

View file

@ -130,10 +130,14 @@ public class MoodleMockupRestTemplateFactory implements MoodleRestTemplateFactor
}
@Override
public String postToMoodleAPIFunction(String functionName, String body) {
public String postToMoodleAPIFunction(
final String functionName,
final MultiValueMap<String, String> queryParams,
final Map<String, Map<String, String>> queryAttributes) {
return null;
}
@Override
public String callMoodleAPIFunction(
final String functionName,

View file

@ -14,6 +14,7 @@ import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Collections;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.ExamSessionCacheService;
import org.junit.Test;
import org.mockito.Mockito;
@ -84,7 +85,7 @@ public class ExamJITSIProctoringServiceTest {
final Cryptor cryptorMock = Mockito.mock(Cryptor.class);
Mockito.when(cryptorMock.decrypt(Mockito.any())).thenReturn(Result.of("fbvgeghergrgrthrehreg123"));
final JitsiProctoringService examJITSIProctoringService =
new JitsiProctoringService(null, null, cryptorMock, null, new JSONMapper(), null);
new JitsiProctoringService(null, null, null, cryptorMock, null, new JSONMapper(), null);
String accessToken = examJITSIProctoringService.createPayload(
"test-app",
@ -118,7 +119,7 @@ public class ExamJITSIProctoringServiceTest {
final Cryptor cryptorMock = Mockito.mock(Cryptor.class);
Mockito.when(cryptorMock.decrypt(Mockito.any())).thenReturn(Result.of("fbvgeghergrgrthrehreg123"));
final JitsiProctoringService examJITSIProctoringService =
new JitsiProctoringService(null, null, cryptorMock, null, new JSONMapper(), null);
new JitsiProctoringService(null, null, null, cryptorMock, null, new JSONMapper(), null);
final ProctoringRoomConnection data = examJITSIProctoringService.createProctoringConnection(
"connectionToken",
"https://seb-jitsi.example.ch",
@ -154,7 +155,7 @@ public class ExamJITSIProctoringServiceTest {
final AuthorizationService authorizationService = Mockito.mock(AuthorizationService.class);
Mockito.when(authorizationService.getUserService()).thenReturn(userService);
final ExamSessionService examSessionService = Mockito.mock(ExamSessionService.class);
final ExamSessionCacheService examSessionCacheService = Mockito.mock(ExamSessionCacheService.class);
final Cryptor cryptor = Mockito.mock(Cryptor.class);
Mockito.when(cryptor.decrypt(Mockito.any())).thenReturn(Result.of("pwd"));
@ -164,7 +165,8 @@ public class ExamJITSIProctoringServiceTest {
return new JitsiProctoringService(
authorizationService,
examSessionService,
null,
examSessionCacheService,
cryptor,
clientHttpRequestFactoryService,
jsonMapper, null);