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