SEBSERV-301 implementation

This commit is contained in:
anhefti 2022-12-21 09:51:14 +01:00
parent d866b219fa
commit aa040fc615
27 changed files with 2210 additions and 1566 deletions

View file

@ -71,8 +71,6 @@ public final class Exam implements GrantEntity {
public static final String ADDITIONAL_ATTR_SIGNATURE_KEY_CERT_ALIAS = "SIGNATURE_KEY_CERT_ALIAS"; 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 */ /** 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"; 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 { public enum ExamStatus {
UP_COMING, UP_COMING,

View file

@ -11,6 +11,7 @@ package ch.ethz.seb.sebserver.gbl.model.exam;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime; import org.joda.time.DateTime;
@ -211,6 +212,23 @@ public final class QuizData implements GrantEntity {
return this.additionalAttributes; 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 @Override
public String toString() { public String toString() {
final StringBuilder builder = new StringBuilder(); final StringBuilder builder = new StringBuilder();

View file

@ -10,6 +10,9 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.exam;
public interface ExamConfigurationValueService { 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 /** Get the actual SEB settings attribute value for the exam configuration mapped as default configuration
* to the given exam * to the given exam
* *
@ -18,4 +21,8 @@ public interface ExamConfigurationValueService {
* @return The current value of the above SEB settings attribute and given exam. */ * @return The current value of the above SEB settings attribute and given exam. */
String getMappedDefaultConfigAttributeValue(Long examId, String configAttributeName); String getMappedDefaultConfigAttributeValue(Long examId, String configAttributeName);
String getQuitSecret(Long examId);
String getQuitLink(Long examId);
} }

View file

@ -276,7 +276,7 @@ public class ExamAdminServiceImpl implements ExamAdminService {
this.additionalAttributesDAO.saveAdditionalAttribute( this.additionalAttributesDAO.saveAdditionalAttribute(
EntityType.EXAM, EntityType.EXAM,
exam.id, exam.id,
Exam.ADDITIONAL_ATTR_ALTERNATIVE_SEB_BEK, SEBRestrictionService.ADDITIONAL_ATTR_ALTERNATIVE_SEB_BEK,
moodleBEK).getOrThrow(); moodleBEK).getOrThrow();
} catch (final Exception e) { } catch (final Exception e) {
log.error("Failed to create additional moodle SEB BEK attribute: ", e); log.error("Failed to create additional moodle SEB BEK attribute: ", e);

View file

@ -8,17 +8,23 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.exam.impl; package ch.ethz.seb.sebserver.webservice.servicelayer.exam.impl;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service; 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.ConfigurationAttributeDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationDAO; 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.ConfigurationValueDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamConfigurationMapDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamConfigurationMapDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamConfigurationValueService; import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamConfigurationValueService;
@Lazy
@Service @Service
@WebServiceProfile
public class ExamConfigurationValueServiceImpl implements ExamConfigurationValueService { public class ExamConfigurationValueServiceImpl implements ExamConfigurationValueService {
private static final Logger log = LoggerFactory.getLogger(ExamConfigurationValueServiceImpl.class); private static final Logger log = LoggerFactory.getLogger(ExamConfigurationValueServiceImpl.class);
@ -27,17 +33,20 @@ public class ExamConfigurationValueServiceImpl implements ExamConfigurationValue
private final ConfigurationDAO configurationDAO; private final ConfigurationDAO configurationDAO;
private final ConfigurationAttributeDAO configurationAttributeDAO; private final ConfigurationAttributeDAO configurationAttributeDAO;
private final ConfigurationValueDAO configurationValueDAO; private final ConfigurationValueDAO configurationValueDAO;
private final Cryptor cryptor;
public ExamConfigurationValueServiceImpl( public ExamConfigurationValueServiceImpl(
final ExamConfigurationMapDAO examConfigurationMapDAO, final ExamConfigurationMapDAO examConfigurationMapDAO,
final ConfigurationDAO configurationDAO, final ConfigurationDAO configurationDAO,
final ConfigurationAttributeDAO configurationAttributeDAO, final ConfigurationAttributeDAO configurationAttributeDAO,
final ConfigurationValueDAO configurationValueDAO) { final ConfigurationValueDAO configurationValueDAO,
final Cryptor cryptor) {
this.examConfigurationMapDAO = examConfigurationMapDAO; this.examConfigurationMapDAO = examConfigurationMapDAO;
this.configurationDAO = configurationDAO; this.configurationDAO = configurationDAO;
this.configurationAttributeDAO = configurationAttributeDAO; this.configurationAttributeDAO = configurationAttributeDAO;
this.configurationValueDAO = configurationValueDAO; this.configurationValueDAO = configurationValueDAO;
this.cryptor = cryptor;
} }
@Override @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;
}
}
} }

View file

@ -8,8 +8,8 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.lms; package ch.ethz.seb.sebserver.webservice.servicelayer.lms;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
@ -113,10 +113,20 @@ public interface CourseAccessAPI {
* @return Result referencing to the Chapters model for the given course or to an error when happened. */ * @return Result referencing to the Chapters model for the given course or to an error when happened. */
Result<Chapters> getCourseChapters(String courseId); Result<Chapters> 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 { static class AsyncQuizFetchBuffer {
public List<QuizData> buffer = new ArrayList<>();
/** The buffer set where already fetched data is stored and can be get */
public Set<QuizData> buffer = new HashSet<>();
/** Indicates whether the asynchronous fetch is still running or has finished */
public boolean finished = false; public boolean finished = false;
/** Indicates if the fetch is been canceled. Set this to true to cancel the asynchronous process */
public boolean canceled = false; public boolean canceled = false;
/** Reference to an error when the asynchronous fetch stopped with an error */
public Exception error = null; public Exception error = null;
public void finish() { public void finish() {

View file

@ -18,6 +18,9 @@ public interface SEBRestrictionService {
String SEB_RESTRICTION_ADDITIONAL_PROPERTY_NAME_PREFIX = "sebRestrictionProp_"; String SEB_RESTRICTION_ADDITIONAL_PROPERTY_NAME_PREFIX = "sebRestrictionProp_";
String SEB_RESTRICTION_ADDITIONAL_PROPERTY_CONFIG_KEY = "config_key"; 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 */ /** Get the LmsAPIService that is used by the SEBRestrictionService */
LmsAPIService getLmsAPIService(); LmsAPIService getLmsAPIService();

View file

@ -98,9 +98,14 @@ public abstract class AbstractCachedCourseAccess {
/** Put all QuizData to short time cache. /** Put all QuizData to short time cache.
* *
* @param quizData Collection of QuizData */ * @param quizData Collection of QuizData
protected void putToCache(final Collection<QuizData> quizData) { * @return the given collection of QuizData */
protected final Collection<QuizData> putToCache(final Collection<QuizData> quizData) {
if (quizData == null || quizData.isEmpty()) {
return quizData;
}
quizData.stream().forEach(q -> this.cache.put(createCacheKey(q.id), q)); quizData.stream().forEach(q -> this.cache.put(createCacheKey(q.id), q));
return quizData;
} }
protected void evict(final String id) { protected void evict(final String id) {

View file

@ -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<String> getKnownTokenAccessPaths() {
final Set<String> paths = new HashSet<>();
paths.add(MoodleAPIRestTemplate.MOODLE_DEFAULT_TOKEN_REQUEST_PATH);
return paths;
}
@Override
public Result<MoodleAPIRestTemplate> createRestTemplate() {
return Result.of(new MockupMoodleRestTemplate(this.apiTemplateDataSupplier.getLmsSetup().lmsApiUrl));
}
@Override
public Result<MoodleAPIRestTemplate> 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<String, String> queryAttributes) {
return callMoodleAPIFunction(functionName, null, queryAttributes);
}
@Override
public String callMoodleAPIFunction(
final String functionName,
final MultiValueMap<String, String> queryParams,
final MultiValueMap<String, String> 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<String, String> queryAttributes) {
try {
final List<String> ids = queryAttributes.get(MoodlePluginCourseAccess.CRITERIA_COURSE_IDS);
final String from = queryAttributes.getFirst(MoodlePluginCourseAccess.CRITERIA_LIMIT_FROM);
System.out.println("************* from: " + from);
final List<MockCD> 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<String, Object> 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<String, String> queryAttributes) {
try {
final List<String> ids = queryAttributes.get(MoodlePluginCourseAccess.CRITERIA_COURSE_IDS);
final List<MockQ> 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<String, Object> 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<String, String> queryAttributes) {
// TODO
return "";
}
}
}

View file

@ -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;
}
}
}

View file

@ -1,463 +1,29 @@
/* /*
* Copyright (c) 2020 ETH Zürich, Educational Development and Technology (LET) * Copyright (c) 2022 ETH Zürich, Educational Development and Technology (LET)
* *
* This Source Code Form is subject to the terms of the Mozilla Public * 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 * 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/. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/ */
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle; package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle;
import java.util.ArrayList; import java.util.Set;
import java.util.Arrays;
import java.util.Collection; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
import java.util.Collections; import ch.ethz.seb.sebserver.gbl.util.Result;
import java.util.HashMap; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier;
import java.util.HashSet;
import java.util.List; public interface MoodleRestTemplateFactory {
import java.util.Map;
import java.util.Set; LmsSetupTestResult test();
import java.util.function.Function;
import java.util.stream.Collectors; APITemplateDataSupplier getApiTemplateDataSupplier();
import org.apache.commons.lang3.StringUtils; Set<String> getKnownTokenAccessPaths();
import org.slf4j.Logger;
import org.slf4j.LoggerFactory; Result<MoodleAPIRestTemplate> createRestTemplate();
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders; Result<MoodleAPIRestTemplate> createRestTemplate(final String accessTokenPath);
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<String> 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<String> 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<APIMessage> 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<MoodleAPIRestTemplate> 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<MoodleAPIRestTemplate> 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<String, String> 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<String> 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<String, String> queryAttributes) {
return callMoodleAPIFunction(functionName, null, queryAttributes);
}
@Override
public String callMoodleAPIFunction(
final String functionName,
final MultiValueMap<String, String> queryParams,
final MultiValueMap<String, String> 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<String> 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<String> 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<String, FunctionInfo> functions;
@JsonCreator
protected WebserviceInfo(
@JsonProperty(value = "username") final String username,
@JsonProperty(value = "userid") final String userid,
@JsonProperty(value = "functions") final Collection<FunctionInfo> 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;
}
}
}

View file

@ -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<String> 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<String> 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<String> 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<APIMessage> 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<MoodleAPIRestTemplate> 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<MoodleAPIRestTemplate> 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<String, String> 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<String> 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<String, String> queryAttributes) {
return callMoodleAPIFunction(functionName, null, queryAttributes);
}
@Override
public String callMoodleAPIFunction(
final String functionName,
final MultiValueMap<String, String> queryParams,
final MultiValueMap<String, String> 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<String> 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<String> 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<String, FunctionInfo> functions;
@JsonCreator
protected WebserviceInfo(
@JsonProperty(value = "username") final String username,
@JsonProperty(value = "userid") final String userid,
@JsonProperty(value = "functions") final Collection<FunctionInfo> 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;
}
}
}

View file

@ -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<Warning> 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<CourseData> 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<CourseQuiz> 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<QuizData> quizDataOf(
final LmsSetup lmsSetup,
final CourseData courseData,
final String uriPrefix,
final boolean prependShortCourseName) {
final Map<String, String> additionalAttrs = new HashMap<>();
additionalAttrs.put(QuizData.ATTR_ADDITIONAL_CREATION_TIME, String.valueOf(courseData.time_created));
additionalAttrs.put(QuizData.ATTR_ADDITIONAL_SHORT_NAME, courseData.short_name);
additionalAttrs.put(QuizData.ATTR_ADDITIONAL_ID_NUMBER, courseData.idnumber);
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<QuizData> courseAndQuiz = courseData.quizzes
.stream()
.map(courseQuizData -> {
final String startURI = uriPrefix + courseQuizData.course_module;
additionalAttrs.put(QuizData.ATTR_ADDITIONAL_TIME_LIMIT, String.valueOf(courseQuizData.time_limit));
return new QuizData(
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<String> quizIds,
final Map<String, CourseData> 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<CourseQuiz> 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<CourseData> courses;
public final Collection<Warning> warnings;
@JsonCreator
public Courses(
@JsonProperty(value = "courses") final Collection<CourseData> courses,
@JsonProperty(value = "warnings") final Collection<Warning> warnings) {
this.courses = courses;
this.warnings = warnings;
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
public static final class CourseQuizData {
public final Collection<CourseQuiz> quizzes;
public final Collection<Warning> warnings;
@JsonCreator
public CourseQuizData(
@JsonProperty(value = "quizzes") final Collection<CourseQuiz> quizzes,
@JsonProperty(value = "warnings") final Collection<Warning> 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<String, String> customfields;
@JsonCreator
public MoodlePluginUserDetails(
final String id,
final String username,
final String firstname,
final String lastname,
final String idnumber,
final String email,
final Map<String, String> 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<CourseKey> courseKeys;
public final Collection<Warning> warnings;
public CoursePage(
@JsonProperty(value = "courses") final Collection<CourseKey> courseKeys,
@JsonProperty(value = "warnings") final Collection<Warning> 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;
}
}
}

View file

@ -17,24 +17,18 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime; import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.core.env.Environment; import org.springframework.core.env.Environment;
import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap; 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.JsonParseException;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonMappingException; 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.CourseAccessAPI;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService; 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;
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.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.CourseDataShort;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.legacy.MoodleCourseDataAsyncLoader.CourseQuizShort; 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"; static final String MOODLE_COURSE_API_SEARCH_PAGE_SIZE = "perpage";
private final JSONMapper jsonMapper; private final JSONMapper jsonMapper;
private final MoodleRestTemplateFactory moodleRestTemplateFactory; private final MoodleRestTemplateFactory restTemplateFactory;
private final MoodleCourseDataAsyncLoader moodleCourseDataAsyncLoader; private final MoodleCourseDataAsyncLoader moodleCourseDataAsyncLoader;
private final boolean prependShortCourseName; private final boolean prependShortCourseName;
private final CircuitBreaker<String> protectedMoodlePageCall; private final CircuitBreaker<String> protectedMoodlePageCall;
@ -110,13 +109,13 @@ public class MoodleCourseAccess implements CourseAccessAPI {
public MoodleCourseAccess( public MoodleCourseAccess(
final JSONMapper jsonMapper, final JSONMapper jsonMapper,
final AsyncService asyncService, final AsyncService asyncService,
final MoodleRestTemplateFactory moodleRestTemplateFactory, final MoodleRestTemplateFactory restTemplateFactory,
final MoodleCourseDataAsyncLoader moodleCourseDataAsyncLoader, final MoodleCourseDataAsyncLoader moodleCourseDataAsyncLoader,
final Environment environment) { final Environment environment) {
this.jsonMapper = jsonMapper; this.jsonMapper = jsonMapper;
this.moodleCourseDataAsyncLoader = moodleCourseDataAsyncLoader; this.moodleCourseDataAsyncLoader = moodleCourseDataAsyncLoader;
this.moodleRestTemplateFactory = moodleRestTemplateFactory; this.restTemplateFactory = restTemplateFactory;
this.prependShortCourseName = BooleanUtils.toBoolean(environment.getProperty( this.prependShortCourseName = BooleanUtils.toBoolean(environment.getProperty(
"sebserver.webservice.lms.moodle.prependShortCourseName", "sebserver.webservice.lms.moodle.prependShortCourseName",
@ -142,12 +141,12 @@ public class MoodleCourseAccess implements CourseAccessAPI {
} }
APITemplateDataSupplier getApiTemplateDataSupplier() { APITemplateDataSupplier getApiTemplateDataSupplier() {
return this.moodleRestTemplateFactory.apiTemplateDataSupplier; return this.restTemplateFactory.getApiTemplateDataSupplier();
} }
@Override @Override
public LmsSetupTestResult testCourseAccessAPI() { public LmsSetupTestResult testCourseAccessAPI() {
final LmsSetupTestResult attributesCheck = this.moodleRestTemplateFactory.test(); final LmsSetupTestResult attributesCheck = this.restTemplateFactory.test();
if (!attributesCheck.isOk()) { if (!attributesCheck.isOk()) {
return attributesCheck; return attributesCheck;
} }
@ -155,7 +154,7 @@ public class MoodleCourseAccess implements CourseAccessAPI {
final Result<MoodleAPIRestTemplate> restTemplateRequest = getRestTemplate(); final Result<MoodleAPIRestTemplate> restTemplateRequest = getRestTemplate();
if (restTemplateRequest.hasError()) { if (restTemplateRequest.hasError()) {
final String message = "Failed to gain access token from Moodle Rest API:\n tried token endpoints: " + 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()); log.error(message + " cause: {}", restTemplateRequest.getError().getMessage());
return LmsSetupTestResult.ofTokenRequestError(LmsType.MOODLE, message); return LmsSetupTestResult.ofTokenRequestError(LmsType.MOODLE, message);
} }
@ -189,12 +188,12 @@ public class MoodleCourseAccess implements CourseAccessAPI {
try { try {
int page = 0; int page = 0;
final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup(); final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup();
final MoodleAPIRestTemplate restTemplate = getRestTemplate().getOrThrow();
final String urlPrefix = (lmsSetup.lmsApiUrl.endsWith(Constants.URL_PATH_SEPARATOR)) final String urlPrefix = (lmsSetup.lmsApiUrl.endsWith(Constants.URL_PATH_SEPARATOR))
? lmsSetup.lmsApiUrl + MOODLE_QUIZ_START_URL_PATH ? lmsSetup.lmsApiUrl + MOODLE_QUIZ_START_URL_PATH
: lmsSetup.lmsApiUrl + Constants.URL_PATH_SEPARATOR + MOODLE_QUIZ_START_URL_PATH; : lmsSetup.lmsApiUrl + Constants.URL_PATH_SEPARATOR + MOODLE_QUIZ_START_URL_PATH;
while (!asyncQuizFetchBuffer.finished && !asyncQuizFetchBuffer.canceled) { while (!asyncQuizFetchBuffer.finished && !asyncQuizFetchBuffer.canceled) {
final MoodleAPIRestTemplate restTemplate = getRestTemplate().getOrThrow();
// first get courses from Moodle for page // first get courses from Moodle for page
final Map<String, CourseData> courseData = new HashMap<>(); final Map<String, CourseData> courseData = new HashMap<>();
final Collection<CourseData> coursesPage = getCoursesPage(restTemplate, page, this.pageSize); final Collection<CourseData> coursesPage = getCoursesPage(restTemplate, page, this.pageSize);
@ -236,15 +235,10 @@ public class MoodleCourseAccess implements CourseAccessAPI {
} }
if (courseQuizData.warnings != null && !courseQuizData.warnings.isEmpty()) { if (courseQuizData.warnings != null && !courseQuizData.warnings.isEmpty()) {
log.warn( MoodleUtils.logMoodleWarning(
"There are warnings from Moodle response: Moodle: {} request: {} warnings: {} warning sample: {}", courseQuizData.warnings,
lmsSetup.name, lmsSetup.name,
MoodleCourseAccess.MOODLE_QUIZ_API_FUNCTION_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());
}
} }
if (courseQuizData.quizzes == null || courseQuizData.quizzes.isEmpty()) { if (courseQuizData.quizzes == null || courseQuizData.quizzes.isEmpty()) {
@ -255,7 +249,7 @@ public class MoodleCourseAccess implements CourseAccessAPI {
courseQuizData.quizzes courseQuizData.quizzes
.stream() .stream()
.filter(getQuizFilter()) .filter(MoodleUtils.getQuizFilter())
.forEach(quiz -> { .forEach(quiz -> {
final CourseData data = courseData.get(quiz.course); final CourseData data = courseData.get(quiz.course);
if (data != null) { if (data != null) {
@ -266,10 +260,16 @@ public class MoodleCourseAccess implements CourseAccessAPI {
courseData.values().stream() courseData.values().stream()
.filter(c -> !c.quizzes.isEmpty()) .filter(c -> !c.quizzes.isEmpty())
.forEach(c -> asyncQuizFetchBuffer.buffer.addAll( .forEach(c -> asyncQuizFetchBuffer.buffer.addAll(
quizDataOf(lmsSetup, c, urlPrefix).stream() MoodleUtils.quizDataOf(lmsSetup, c, urlPrefix, this.prependShortCourseName)
.stream()
.filter(LmsAPIService.quizFilterPredicate(filterMap)) .filter(LmsAPIService.quizFilterPredicate(filterMap))
.collect(Collectors.toList()))); .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++; page++;
} }
@ -298,8 +298,8 @@ public class MoodleCourseAccess implements CourseAccessAPI {
final Map<String, CourseDataShort> cachedCourseData = this.moodleCourseDataAsyncLoader final Map<String, CourseDataShort> cachedCourseData = this.moodleCourseDataAsyncLoader
.getCachedCourseData(); .getCachedCourseData();
final String courseId = getCourseId(id); final String courseId = MoodleUtils.getCourseId(id);
final String quizId = getQuizId(id); final String quizId = MoodleUtils.getQuizId(id);
if (cachedCourseData.containsKey(courseId)) { if (cachedCourseData.containsKey(courseId)) {
final CourseDataShort courseData = cachedCourseData.get(courseId); final CourseDataShort courseData = cachedCourseData.get(courseId);
final CourseQuizShort quiz = courseData.quizzes final CourseQuizShort quiz = courseData.quizzes
@ -352,7 +352,7 @@ public class MoodleCourseAccess implements CourseAccessAPI {
MOODLE_USER_PROFILE_API_FUNCTION_NAME, MOODLE_USER_PROFILE_API_FUNCTION_NAME,
queryAttributes); queryAttributes);
if (checkAccessDeniedError(userDetailsJSON)) { if (MoodleUtils.checkAccessDeniedError(userDetailsJSON)) {
final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup(); final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup();
log.error("Get access denied error from Moodle: {} for API call: {}, response: {}", log.error("Get access denied error from Moodle: {} for API call: {}, response: {}",
lmsSetup, lmsSetup,
@ -486,7 +486,7 @@ public class MoodleCourseAccess implements CourseAccessAPI {
final Map<String, CourseData> courseData = getCoursesForIds( final Map<String, CourseData> courseData = getCoursesForIds(
restTemplate, restTemplate,
quizIds.stream() quizIds.stream()
.map(MoodleCourseAccess::getCourseId) .map(MoodleUtils::getCourseId)
.collect(Collectors.toSet())) .collect(Collectors.toSet()))
.stream() .stream()
.collect(Collectors.toMap(cd -> cd.id, Function.identity())); .collect(Collectors.toMap(cd -> cd.id, Function.identity()));
@ -518,7 +518,12 @@ public class MoodleCourseAccess implements CourseAccessAPI {
return Collections.emptyList(); 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()) { if (courseQuizData.quizzes == null || courseQuizData.quizzes.isEmpty()) {
log.error("No quizzes found for ids: {} on LMS; {}", quizIds, lmsSetup.name); log.error("No quizzes found for ids: {} on LMS; {}", quizIds, lmsSetup.name);
@ -528,7 +533,7 @@ public class MoodleCourseAccess implements CourseAccessAPI {
final Map<String, CourseData> finalCourseDataRef = courseData; final Map<String, CourseData> finalCourseDataRef = courseData;
courseQuizData.quizzes courseQuizData.quizzes
.stream() .stream()
.forEach(quiz -> fillSelectedQuizzes(quizIds, finalCourseDataRef, quiz)); .forEach(quiz -> MoodleUtils.fillSelectedQuizzes(quizIds, finalCourseDataRef, quiz));
final String urlPrefix = (lmsSetup.lmsApiUrl.endsWith(Constants.URL_PATH_SEPARATOR)) final String urlPrefix = (lmsSetup.lmsApiUrl.endsWith(Constants.URL_PATH_SEPARATOR))
? lmsSetup.lmsApiUrl + MOODLE_QUIZ_START_URL_PATH ? lmsSetup.lmsApiUrl + MOODLE_QUIZ_START_URL_PATH
@ -537,7 +542,11 @@ public class MoodleCourseAccess implements CourseAccessAPI {
return courseData.values() return courseData.values()
.stream() .stream()
.filter(c -> !c.quizzes.isEmpty()) .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()); .collect(Collectors.toList());
} catch (final Exception e) { } catch (final Exception e) {
@ -546,27 +555,6 @@ public class MoodleCourseAccess implements CourseAccessAPI {
} }
} }
private void fillSelectedQuizzes(
final Set<String> quizIds,
final Map<String, CourseData> 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<CourseData> getCoursesForIds( private Collection<CourseData> getCoursesForIds(
final MoodleAPIRestTemplate restTemplate, final MoodleAPIRestTemplate restTemplate,
final Set<String> ids) { final Set<String> ids) {
@ -596,7 +584,12 @@ public class MoodleCourseAccess implements CourseAccessAPI {
return Collections.emptyList(); 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()) { if (courses.courses == null || courses.courses.isEmpty()) {
log.error("No courses found for ids: {} on LMS: {}", ids, lmsSetup.name); log.error("No courses found for ids: {} on LMS: {}", ids, lmsSetup.name);
@ -610,51 +603,6 @@ public class MoodleCourseAccess implements CourseAccessAPI {
} }
} }
private List<QuizData> quizDataOf(
final LmsSetup lmsSetup,
final CourseData courseData,
final String uriPrefix) {
final Map<String, String> additionalAttrs = new HashMap<>();
additionalAttrs.put(QuizData.ATTR_ADDITIONAL_CREATION_TIME, String.valueOf(courseData.time_created));
additionalAttrs.put(QuizData.ATTR_ADDITIONAL_SHORT_NAME, courseData.short_name);
additionalAttrs.put(QuizData.ATTR_ADDITIONAL_ID_NUMBER, courseData.idnumber);
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<QuizData> courseAndQuiz = courseData.quizzes
.stream()
.map(courseQuizData -> {
final String startURI = uriPrefix + courseQuizData.course_module;
additionalAttrs.put(QuizData.ATTR_ADDITIONAL_TIME_LIMIT, String.valueOf(courseQuizData.time_limit));
return new QuizData(
getInternalQuizId(
courseQuizData.course_module,
courseData.id,
courseData.short_name,
courseData.idnumber),
lmsSetup.getInstitutionId(),
lmsSetup.id,
lmsSetup.getLmsType(),
(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<QuizData> quizDataOf( private List<QuizData> quizDataOf(
final LmsSetup lmsSetup, final LmsSetup lmsSetup,
final CourseDataShort courseData, final CourseDataShort courseData,
@ -682,7 +630,7 @@ public class MoodleCourseAccess implements CourseAccessAPI {
final String startURI = uriPrefix + courseQuizData.course_module; final String startURI = uriPrefix + courseQuizData.course_module;
return new QuizData( return new QuizData(
getInternalQuizId( MoodleUtils.getInternalQuizId(
courseQuizData.course_module, courseQuizData.course_module,
courseData.id, courseData.id,
courseData.short_name, courseData.short_name,
@ -706,7 +654,7 @@ public class MoodleCourseAccess implements CourseAccessAPI {
private Result<MoodleAPIRestTemplate> getRestTemplate() { private Result<MoodleAPIRestTemplate> getRestTemplate() {
if (this.restTemplate == null) { if (this.restTemplate == null) {
final Result<MoodleAPIRestTemplate> templateRequest = this.moodleRestTemplateFactory final Result<MoodleAPIRestTemplate> templateRequest = this.restTemplateFactory
.createRestTemplate(); .createRestTemplate();
if (templateRequest.hasError()) { if (templateRequest.hasError()) {
return templateRequest; return templateRequest;
@ -718,95 +666,6 @@ public class MoodleCourseAccess implements CourseAccessAPI {
return Result.of(this.restTemplate); 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<Warning> 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<CourseData> getCoursesPage( private Collection<CourseData> getCoursesPage(
final MoodleAPIRestTemplate restTemplate, final MoodleAPIRestTemplate restTemplate,
final int page, final int page,
@ -866,7 +725,7 @@ public class MoodleCourseAccess implements CourseAccessAPI {
final Collection<CourseData> result = getCoursesForIds(restTemplate, ids) final Collection<CourseData> result = getCoursesForIds(restTemplate, ids)
.stream() .stream()
.filter(getCourseFilter()) .filter(MoodleUtils.getCourseFilter())
.collect(Collectors.toList()); .collect(Collectors.toList());
if (log.isDebugEnabled()) { if (log.isDebugEnabled()) {
@ -882,258 +741,4 @@ public class MoodleCourseAccess implements CourseAccessAPI {
} }
} }
private Predicate<CourseData> 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<CourseQuiz> 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<CourseQuiz> 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<CourseData> courses;
final Collection<Warning> warnings;
@JsonCreator
protected Courses(
@JsonProperty(value = "courses") final Collection<CourseData> courses,
@JsonProperty(value = "warnings") final Collection<Warning> warnings) {
this.courses = courses;
this.warnings = warnings;
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
private static final class CourseQuizData {
final Collection<CourseQuiz> quizzes;
final Collection<Warning> warnings;
@JsonCreator
protected CourseQuizData(
@JsonProperty(value = "quizzes") final Collection<CourseQuiz> quizzes,
@JsonProperty(value = "warnings") final Collection<Warning> 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<CourseKey> courseKeys;
final Collection<Warning> warnings;
public CoursePage(
@JsonProperty(value = "courses") final Collection<CourseKey> courseKeys,
@JsonProperty(value = "warnings") final Collection<Warning> 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();
}
}
} }

View file

@ -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.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;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleAPIRestTemplate.Warning; 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 @Lazy
@Component @Component

View file

@ -8,408 +8,34 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.legacy; 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.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.exam.SEBRestriction;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.util.Result; 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.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: /** Dummy Implementation */
* http://yourmoodle.org/webservice/rest/server.php?wstoken={token}&moodlewsrestformat=json&wsfunction=seb_restriction&courseId=123
*
* Response (JSON):
*
* <pre>
* {
* "quizId": "456",
* "configKeys": [
* "key1",
* "key2",
* "key3"
* ],
* "browserKeys": [
* "bkey1",
* "bkey2",
* "bkey3"
* ]
* }
* </pre>
*
* 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 */
public class MoodleCourseRestriction implements SEBRestrictionAPI { 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 @Override
public LmsSetupTestResult testCourseRestrictionAPI() { public LmsSetupTestResult testCourseRestrictionAPI() {
// try to call the SEB Restrictions API return LmsSetupTestResult.ofQuizAccessAPIError(LmsType.MOODLE, "SEB restriction not supported");
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);
} }
@Override @Override
public Result<SEBRestriction> getSEBClientRestriction(final Exam exam) { public Result<SEBRestriction> getSEBClientRestriction(final Exam exam) {
return Result.tryCatch(() -> { return Result.ofError(new UnsupportedOperationException("SEB restriction not supported"));
return getSEBRestriction(
MoodleCourseAccess.getQuizId(exam.externalId),
MoodleCourseAccess.getShortname(exam.externalId),
MoodleCourseAccess.getIdnumber(exam.externalId))
.map(restriction -> SEBRestriction.from(exam.id, restriction))
.getOrThrow();
});
} }
@Override @Override
public Result<SEBRestriction> applySEBClientRestriction( public Result<SEBRestriction> applySEBClientRestriction(final Exam exam, final SEBRestriction sebRestrictionData) {
final Exam exam, return Result.ofError(new UnsupportedOperationException("SEB restriction not supported"));
final SEBRestriction sebRestrictionData) {
return this.updateSEBRestriction(
exam.externalId,
MoodleSEBRestriction.from(sebRestrictionData))
.map(result -> sebRestrictionData);
} }
@Override @Override
public Result<Exam> releaseSEBClientRestriction(final Exam exam) { public Result<Exam> releaseSEBClientRestriction(final Exam exam) {
return this.deleteSEBRestriction(exam.externalId) return Result.ofError(new UnsupportedOperationException("SEB restriction not supported"));
.map(result -> exam);
}
Result<MoodleSEBRestriction> 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<String, String> 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<MoodleSEBRestriction>() {
});
return restrictiondata;
});
}
Result<MoodleSEBRestriction> createSEBRestriction(
final String internalId,
final MoodleSEBRestriction restriction) {
return Result.tryCatch(() -> {
return createSEBRestriction(
MoodleCourseAccess.getQuizId(internalId),
MoodleCourseAccess.getShortname(internalId),
MoodleCourseAccess.getIdnumber(internalId),
restriction)
.getOrThrow();
});
}
Result<MoodleSEBRestriction> 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<MoodleSEBRestriction> updateSEBRestriction(
final String internalId,
final MoodleSEBRestriction restriction) {
return Result.tryCatch(() -> {
return updateSEBRestriction(
MoodleCourseAccess.getQuizId(internalId),
MoodleCourseAccess.getShortname(internalId),
MoodleCourseAccess.getIdnumber(internalId),
restriction)
.getOrThrow();
});
}
Result<MoodleSEBRestriction> 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<Boolean> deleteSEBRestriction(
final String internalId) {
return Result.tryCatch(() -> {
return deleteSEBRestriction(
MoodleCourseAccess.getQuizId(internalId),
MoodleCourseAccess.getShortname(internalId),
MoodleCourseAccess.getIdnumber(internalId))
.getOrThrow();
});
}
Result<Boolean> 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<String, String> 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<MoodleAPIRestTemplate> getRestTemplate() {
if (this.restTemplate == null) {
final Result<MoodleAPIRestTemplate> templateRequest = this.moodleRestTemplateFactory
.createRestTemplate();
if (templateRequest.hasError()) {
return templateRequest;
} else {
this.restTemplate = templateRequest.get();
}
}
return Result.of(this.restTemplate);
}
private Result<MoodleSEBRestriction> 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<String, String> 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<String, String> 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<MoodleSEBRestriction>() {
});
return restrictiondata;
});
}
public Error checkError(final String jsonResponse) {
if (jsonResponse.contains("exception") || jsonResponse.contains("errorcode")) {
try {
return this.jsonMapper.readValue(
jsonResponse,
new TypeReference<Error>() {
});
} 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();
}
} }
} }

View file

@ -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.model.institution.LmsSetup.LmsType;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result; 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.APITemplateDataSupplier;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate; 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.LmsAPITemplateFactory;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.LmsAPITemplateAdapter; 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.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.MoodlePluginCourseAccess;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.plugin.MoodlePluginCourseRestriction; 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 Environment environment;
private final ClientCredentialService clientCredentialService; private final ClientCredentialService clientCredentialService;
private final ClientHttpRequestFactoryService clientHttpRequestFactoryService; private final ClientHttpRequestFactoryService clientHttpRequestFactoryService;
private final ExamConfigurationValueService examConfigurationValueService;
private final ApplicationContext applicationContext; private final ApplicationContext applicationContext;
private final String[] alternativeTokenRequestPaths; private final String[] alternativeTokenRequestPaths;
@ -56,6 +59,7 @@ public class MoodleLmsAPITemplateFactory implements LmsAPITemplateFactory {
final AsyncService asyncService, final AsyncService asyncService,
final Environment environment, final Environment environment,
final ClientCredentialService clientCredentialService, final ClientCredentialService clientCredentialService,
final ExamConfigurationValueService examConfigurationValueService,
final ClientHttpRequestFactoryService clientHttpRequestFactoryService, final ClientHttpRequestFactoryService clientHttpRequestFactoryService,
final ApplicationContext applicationContext, final ApplicationContext applicationContext,
@Value("${sebserver.webservice.lms.moodle.api.token.request.paths:}") final String alternativeTokenRequestPaths) { @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.asyncService = asyncService;
this.environment = environment; this.environment = environment;
this.clientCredentialService = clientCredentialService; this.clientCredentialService = clientCredentialService;
this.examConfigurationValueService = examConfigurationValueService;
this.clientHttpRequestFactoryService = clientHttpRequestFactoryService; this.clientHttpRequestFactoryService = clientHttpRequestFactoryService;
this.applicationContext = applicationContext; this.applicationContext = applicationContext;
this.alternativeTokenRequestPaths = (alternativeTokenRequestPaths != null) this.alternativeTokenRequestPaths = (alternativeTokenRequestPaths != null)
@ -88,20 +93,26 @@ public class MoodleLmsAPITemplateFactory implements LmsAPITemplateFactory {
.getBean(MoodleCourseDataAsyncLoader.class); .getBean(MoodleCourseDataAsyncLoader.class);
asyncLoaderPrototype.init(lmsSetup.getModelId()); asyncLoaderPrototype.init(lmsSetup.getModelId());
final MoodleRestTemplateFactory moodleRestTemplateFactory = new MoodleRestTemplateFactory( final MoodleRestTemplateFactory restTemplateFactory = new MoodleRestTemplateFactoryImpl(
this.jsonMapper, this.jsonMapper,
apiTemplateDataSupplier, apiTemplateDataSupplier,
this.clientCredentialService, this.clientCredentialService,
this.clientHttpRequestFactoryService, this.clientHttpRequestFactoryService,
this.alternativeTokenRequestPaths); this.alternativeTokenRequestPaths);
if (this.moodlePluginCheck.checkPluginAvailable(lmsSetup)) { if (this.moodlePluginCheck.checkPluginAvailable(restTemplateFactory)) {
final MoodlePluginCourseAccess moodlePluginCourseAccess = new MoodlePluginCourseAccess( final MoodlePluginCourseAccess moodlePluginCourseAccess = new MoodlePluginCourseAccess(
this.jsonMapper, this.jsonMapper,
moodleRestTemplateFactory, this.asyncService,
this.cacheManager); restTemplateFactory,
final MoodlePluginCourseRestriction moodlePluginCourseRestriction = new MoodlePluginCourseRestriction(); this.cacheManager,
this.environment);
final MoodlePluginCourseRestriction moodlePluginCourseRestriction = new MoodlePluginCourseRestriction(
this.jsonMapper,
restTemplateFactory,
this.examConfigurationValueService);
return new LmsAPITemplateAdapter( return new LmsAPITemplateAdapter(
this.asyncService, this.asyncService,
@ -115,20 +126,16 @@ public class MoodleLmsAPITemplateFactory implements LmsAPITemplateFactory {
final MoodleCourseAccess moodleCourseAccess = new MoodleCourseAccess( final MoodleCourseAccess moodleCourseAccess = new MoodleCourseAccess(
this.jsonMapper, this.jsonMapper,
this.asyncService, this.asyncService,
moodleRestTemplateFactory, restTemplateFactory,
asyncLoaderPrototype, asyncLoaderPrototype,
this.environment); this.environment);
final MoodleCourseRestriction moodleCourseRestriction = new MoodleCourseRestriction(
this.jsonMapper,
moodleRestTemplateFactory);
return new LmsAPITemplateAdapter( return new LmsAPITemplateAdapter(
this.asyncService, this.asyncService,
this.environment, this.environment,
apiTemplateDataSupplier, apiTemplateDataSupplier,
moodleCourseAccess, moodleCourseAccess,
moodleCourseRestriction); new MoodleCourseRestriction());
} }
}); });
} }

View file

@ -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;
}
}

View file

@ -8,64 +8,127 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.plugin; package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.plugin;
import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set; 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.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.cache.CacheManager; 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.core.JsonParseException;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.annotation.JsonProperty; 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.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.Chapters;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData; import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails; import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.CourseAccessAPI; 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.AbstractCachedCourseAccess;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleAPIRestTemplate; 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.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 { public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess implements CourseAccessAPI {
private static final Logger log = LoggerFactory.getLogger(MoodlePluginCourseAccess.class); private static final Logger log = LoggerFactory.getLogger(MoodlePluginCourseAccess.class);
static final String COURSES_API_FUNCTION_NAME = "local_sebserver_get_courses"; public static final String MOODLE_QUIZ_START_URL_PATH = "mod/quiz/view.php?id=";
static final String QUIZZES_BY_COURSES_API_FUNCTION_NAME = "local_sebserver_get_quizzes_by_courses"; public static final String COURSES_API_FUNCTION_NAME = "quizaccess_sebserver_get_courses";
static final String USERS_API_FUNCTION_NAME = "local_sebserver_get_users"; 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"; public static final String ATTR_FIELD = "field";
static final String CRITERIA_TO_DATE = "to_date"; public static final String CRITERIA_COURSE_IDS = "ids";
static final String CRITERIA_LIMIT_FROM = "limitfrom"; public static final String CRITERIA_FROM_DATE = "from_date";
static final String CRITERIA_LIMIT_NUM = "limitnum"; 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 JSONMapper jsonMapper;
private final MoodleRestTemplateFactory moodleRestTemplateFactory; private final MoodleRestTemplateFactory restTemplateFactory;
private final CircuitBreaker<String> protectedMoodlePageCall;
private final boolean prependShortCourseName;
private final int pageSize;
private final int maxSize;
private MoodleAPIRestTemplate restTemplate; private MoodleAPIRestTemplate restTemplate;
public MoodlePluginCourseAccess( public MoodlePluginCourseAccess(
final JSONMapper jsonMapper, final JSONMapper jsonMapper,
final MoodleRestTemplateFactory moodleRestTemplateFactory, final AsyncService asyncService,
final CacheManager cacheManager) { final MoodleRestTemplateFactory restTemplateFactory,
final CacheManager cacheManager,
final Environment environment) {
super(cacheManager); super(cacheManager);
this.jsonMapper = jsonMapper; 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 @Override
public LmsSetupTestResult testCourseAccessAPI() { public LmsSetupTestResult testCourseAccessAPI() {
final LmsSetupTestResult attributesCheck = this.moodleRestTemplateFactory.test(); final LmsSetupTestResult attributesCheck = this.restTemplateFactory.test();
if (!attributesCheck.isOk()) { if (!attributesCheck.isOk()) {
return attributesCheck; return attributesCheck;
} }
@ -73,78 +136,417 @@ public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess impleme
final Result<MoodleAPIRestTemplate> restTemplateRequest = getRestTemplate(); final Result<MoodleAPIRestTemplate> restTemplateRequest = getRestTemplate();
if (restTemplateRequest.hasError()) { if (restTemplateRequest.hasError()) {
final String message = "Failed to gain access token from Moodle Rest API:\n tried token endpoints: " + 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()); log.error(message + " cause: {}", restTemplateRequest.getError().getMessage());
return LmsSetupTestResult.ofTokenRequestError(LmsType.MOODLE_PLUGIN, message); return LmsSetupTestResult.ofTokenRequestError(LmsType.MOODLE_PLUGIN, message);
} }
final MoodleAPIRestTemplate restTemplate = restTemplateRequest.get(); final MoodleAPIRestTemplate restTemplate = restTemplateRequest.get();
// try { try {
// restTemplate.testAPIConnection(
// COURSES_API_FUNCTION_NAME, restTemplate.testAPIConnection(
// QUIZZES_BY_COURSES_API_FUNCTION_NAME, COURSES_API_FUNCTION_NAME,
// USERS_API_FUNCTION_NAME); QUIZZES_BY_COURSES_API_FUNCTION_NAME,
// } catch (final RuntimeException e) { USERS_API_FUNCTION_NAME);
// log.error("Failed to access Moodle course API: ", e);
// return LmsSetupTestResult.ofQuizAccessAPIError(LmsType.MOODLE_PLUGIN, e.getMessage()); } 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); return LmsSetupTestResult.ofOkay(LmsType.MOODLE_PLUGIN);
} }
@Override @Override
public Result<List<QuizData>> getQuizzes(final FilterMap filterMap) { public Result<List<QuizData>> getQuizzes(final FilterMap filterMap) {
System.out.println("***************** filterMap: " + filterMap); return Result.ofError(new UnsupportedOperationException());
// TODO Auto-generated method stub
return Result.of(Collections.emptyList());
}
@Override
public Result<Collection<QuizData>> getQuizzes(final Set<String> ids) {
// TODO Auto-generated method stub
return null;
} }
@Override @Override
public void fetchQuizzes(final FilterMap filterMap, final AsyncQuizFetchBuffer asyncQuizFetchBuffer) { 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<QuizData> 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<Collection<QuizData>> getQuizzes(final Set<String> ids) {
return Result.tryCatch(() -> {
final Set<String> missingIds = new HashSet<>(ids);
final Collection<QuizData> result = new ArrayList<>();
final Set<String> 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 @Override
public Result<QuizData> getQuiz(final String id) { public Result<QuizData> getQuiz(final String id) {
// TODO Auto-generated method stub return Result.tryCatch(() -> {
return null;
final QuizData fromCache = super.getFromCache(id);
if (fromCache != null) {
return fromCache;
}
final Set<String> ids = Stream.of(id).collect(Collectors.toSet());
final Iterator<QuizData> 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 @Override
public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeUserId) { public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeUserId) {
// TODO Auto-generated method stub return Result.tryCatch(() -> {
return null;
final MoodleAPIRestTemplate template = getRestTemplate()
.getOrThrow();
final MultiValueMap<String, String> 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.<MoodlePluginUserDetails[]> readValue(
userDetailsJSON,
new TypeReference<MoodlePluginUserDetails[]>() {
});
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 @Override
public String getExamineeName(final String examineeUserId) { public String getExamineeName(final String examineeUserId) {
// TODO Auto-generated method stub return getExamineeAccountDetails(examineeUserId)
return null; .map(ExamineeAccountDetails::getDisplayName)
.onError(error -> log.warn("Failed to request user-name for ID: {}", error.getMessage(), error))
.getOr(examineeUserId);
} }
@Override @Override
public Result<Chapters> getCourseChapters(final String courseId) { public Result<Chapters> getCourseChapters(final String courseId) {
// TODO Auto-generated method stub return Result.of(new Chapters(Collections.emptyList()));
return null;
} }
@Override private String getLmsSetupName() {
protected Long getLmsSetupId() { return this.restTemplateFactory.getApiTemplateDataSupplier().getLmsSetup().name;
// TODO Auto-generated method stub }
return null;
private void fetchQuizzesPage(
final int page,
final DateTime quizFromTime,
final AsyncQuizFetchBuffer asyncQuizFetchBuffer,
final Predicate<QuizData> 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<String, CourseData> courseData = new HashMap<>();
final Collection<CourseData> 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<String, String> attributes = new LinkedMultiValueMap<>();
final List<String> 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<CourseData> 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<String, String> 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<CourseData> 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<QuizData> getQuizzesForIds(
final MoodleAPIRestTemplate restTemplate,
final Set<String> 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<String, CourseData> 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<String, String> 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<String, CourseData> 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<CourseData> getCoursesForIds(
final MoodleAPIRestTemplate restTemplate,
final Set<String> 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<String, String> 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<MoodleAPIRestTemplate> getRestTemplate() { private Result<MoodleAPIRestTemplate> getRestTemplate() {
if (this.restTemplate == null) { if (this.restTemplate == null) {
final Result<MoodleAPIRestTemplate> templateRequest = this.moodleRestTemplateFactory final Result<MoodleAPIRestTemplate> templateRequest = this.restTemplateFactory
.createRestTemplate(); .createRestTemplate();
if (templateRequest.hasError()) { if (templateRequest.hasError()) {
return templateRequest; return templateRequest;
@ -156,99 +558,4 @@ public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess impleme
return Result.of(this.restTemplate); return Result.of(this.restTemplate);
} }
// ---- Mapping Classes ---
@JsonIgnoreProperties(ignoreUnknown = true)
private static final class Courses {
final Collection<CourseData> courses;
final Collection<Warning> warnings;
@JsonCreator
protected Courses(
@JsonProperty(value = "courses") final Collection<CourseData> courses,
@JsonProperty(value = "warnings") final Collection<Warning> 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<CourseQuiz> 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<CourseQuiz> quizzes;
final Collection<Warning> warnings;
@JsonCreator
protected CourseQuizData(
@JsonProperty(value = "quizzes") final Collection<CourseQuiz> quizzes,
@JsonProperty(value = "warnings") final Collection<Warning> 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;
}
}
} }

View file

@ -8,25 +8,116 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.plugin; 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.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction; 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.LmsSetup.LmsType;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.util.Result; 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.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 { 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 @Override
public LmsSetupTestResult testCourseRestrictionAPI() { public LmsSetupTestResult testCourseRestrictionAPI() {
// TODO Auto-generated method stub
final LmsSetupTestResult attributesCheck = this.restTemplateFactory.test();
if (!attributesCheck.isOk()) {
return attributesCheck;
}
final Result<MoodleAPIRestTemplate> 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); return LmsSetupTestResult.ofOkay(LmsType.MOODLE_PLUGIN);
} }
@Override @Override
public Result<SEBRestriction> getSEBClientRestriction(final Exam exam) { public Result<SEBRestriction> getSEBClientRestriction(final Exam exam) {
// TODO Auto-generated method stub return getRestTemplate().map(restTemplate -> {
return null;
if (log.isDebugEnabled()) {
log.debug("Get SEB Client restriction on exam: {}", exam);
}
final String quizId = MoodleUtils.getQuizId(exam.getExternalId());
final LinkedMultiValueMap<String, String> 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 @Override
@ -34,14 +125,113 @@ public class MoodlePluginCourseRestriction implements SEBRestrictionAPI {
final Exam exam, final Exam exam,
final SEBRestriction sebRestrictionData) { final SEBRestriction sebRestrictionData) {
// TODO Auto-generated method stub return Result.tryCatch(() -> {
return null;
if (log.isDebugEnabled()) {
log.debug("Apply SEB Client restriction on exam: {}", exam);
}
final String quizId = MoodleUtils.getQuizId(exam.getExternalId());
final LinkedMultiValueMap<String, String> addQuery = new LinkedMultiValueMap<>();
addQuery.add(ATTRIBUTE_QUIZ_ID, quizId);
final ArrayList<String> beks = new ArrayList<>(sebRestrictionData.browserExamKeys);
final ArrayList<String> 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<String, String> 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 @Override
public Result<Exam> releaseSEBClientRestriction(final Exam exam) { public Result<Exam> releaseSEBClientRestriction(final Exam exam) {
// TODO Auto-generated method stub return Result.tryCatch(() -> {
return null; if (log.isDebugEnabled()) {
log.debug("Release SEB Client restriction on exam: {}", exam);
}
final String quizId = MoodleUtils.getQuizId(exam.getExternalId());
final LinkedMultiValueMap<String, String> 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<String, String> 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<String> configKeys = Arrays.asList(StringUtils.split(
moodleRestriction.config_keys,
Constants.LIST_SEPARATOR));
final List<String> browserExamKeys = Arrays.asList(StringUtils.split(
moodleRestriction.browser_exam_keys,
Constants.LIST_SEPARATOR));
final Map<String, String> 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<MoodleAPIRestTemplate> getRestTemplate() {
if (this.restTemplate == null) {
final Result<MoodleAPIRestTemplate> templateRequest = this.restTemplateFactory
.createRestTemplate();
if (templateRequest.hasError()) {
return templateRequest;
} else {
this.restTemplate = templateRequest.get();
}
}
return Result.of(this.restTemplate);
} }
} }

View file

@ -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.model.institution.LmsSetup.LmsType;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result; 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.APITemplateDataSupplier;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate; 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.LmsAPITemplateFactory;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.LmsAPITemplateAdapter; 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; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory;
@Lazy @Lazy
@ -41,6 +44,7 @@ public class MooldePluginLmsAPITemplateFactory implements LmsAPITemplateFactory
private final AsyncService asyncService; private final AsyncService asyncService;
private final Environment environment; private final Environment environment;
private final ClientCredentialService clientCredentialService; private final ClientCredentialService clientCredentialService;
private final ExamConfigurationValueService examConfigurationValueService;
private final ClientHttpRequestFactoryService clientHttpRequestFactoryService; private final ClientHttpRequestFactoryService clientHttpRequestFactoryService;
private final ApplicationContext applicationContext; private final ApplicationContext applicationContext;
private final String[] alternativeTokenRequestPaths; private final String[] alternativeTokenRequestPaths;
@ -52,6 +56,7 @@ public class MooldePluginLmsAPITemplateFactory implements LmsAPITemplateFactory
final AsyncService asyncService, final AsyncService asyncService,
final Environment environment, final Environment environment,
final ClientCredentialService clientCredentialService, final ClientCredentialService clientCredentialService,
final ExamConfigurationValueService examConfigurationValueService,
final ClientHttpRequestFactoryService clientHttpRequestFactoryService, final ClientHttpRequestFactoryService clientHttpRequestFactoryService,
final ApplicationContext applicationContext, final ApplicationContext applicationContext,
@Value("${sebserver.webservice.lms.moodle.api.token.request.paths:}") final String alternativeTokenRequestPaths) { @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.asyncService = asyncService;
this.environment = environment; this.environment = environment;
this.clientCredentialService = clientCredentialService; this.clientCredentialService = clientCredentialService;
this.examConfigurationValueService = examConfigurationValueService;
this.clientHttpRequestFactoryService = clientHttpRequestFactoryService; this.clientHttpRequestFactoryService = clientHttpRequestFactoryService;
this.applicationContext = applicationContext; this.applicationContext = applicationContext;
this.alternativeTokenRequestPaths = (alternativeTokenRequestPaths != null) this.alternativeTokenRequestPaths = (alternativeTokenRequestPaths != null)
@ -78,19 +84,27 @@ public class MooldePluginLmsAPITemplateFactory implements LmsAPITemplateFactory
public Result<LmsAPITemplate> create(final APITemplateDataSupplier apiTemplateDataSupplier) { public Result<LmsAPITemplate> create(final APITemplateDataSupplier apiTemplateDataSupplier) {
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
final MoodleRestTemplateFactory moodleRestTemplateFactory = new MoodleRestTemplateFactory( // final MoodleRestTemplateFactory moodleRestTemplateFactory = new MoodleRestTemplateFactoryImpl(
this.jsonMapper, // this.jsonMapper,
apiTemplateDataSupplier, // apiTemplateDataSupplier,
this.clientCredentialService, // this.clientCredentialService,
this.clientHttpRequestFactoryService, // this.clientHttpRequestFactoryService,
this.alternativeTokenRequestPaths); // this.alternativeTokenRequestPaths);
final MoodleRestTemplateFactory moodleRestTemplateFactory =
new MockupRestTemplateFactory(apiTemplateDataSupplier);
final MoodlePluginCourseAccess moodlePluginCourseAccess = new MoodlePluginCourseAccess( final MoodlePluginCourseAccess moodlePluginCourseAccess = new MoodlePluginCourseAccess(
this.jsonMapper, this.jsonMapper,
this.asyncService,
moodleRestTemplateFactory, 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( return new LmsAPITemplateAdapter(
this.asyncService, this.asyncService,

View file

@ -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.LmsSetup.LmsType;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails; import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails;
import ch.ethz.seb.sebserver.gbl.util.Cryptor;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gbl.util.Utils; import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
@ -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_LINK = "ADDITIONAL_ATTR_QUIT_LINK";
private static final String ADDITIONAL_ATTR_QUIT_SECRET = "ADDITIONAL_ATTR_QUIT_SECRET"; 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 ClientHttpRequestFactoryService clientHttpRequestFactoryService;
private final ClientCredentialService clientCredentialService; private final ClientCredentialService clientCredentialService;
private final APITemplateDataSupplier apiTemplateDataSupplier; private final APITemplateDataSupplier apiTemplateDataSupplier;
private final ExamConfigurationValueService examConfigurationValueService; private final ExamConfigurationValueService examConfigurationValueService;
private final Cryptor cryptor;
private final Long lmsSetupId; private final Long lmsSetupId;
private OlatLmsRestTemplate cachedRestTemplate; private OlatLmsRestTemplate cachedRestTemplate;
@ -85,7 +80,6 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
final ClientCredentialService clientCredentialService, final ClientCredentialService clientCredentialService,
final APITemplateDataSupplier apiTemplateDataSupplier, final APITemplateDataSupplier apiTemplateDataSupplier,
final ExamConfigurationValueService examConfigurationValueService, final ExamConfigurationValueService examConfigurationValueService,
final Cryptor cryptor,
final CacheManager cacheManager) { final CacheManager cacheManager) {
super(cacheManager); super(cacheManager);
@ -94,7 +88,6 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
this.clientCredentialService = clientCredentialService; this.clientCredentialService = clientCredentialService;
this.apiTemplateDataSupplier = apiTemplateDataSupplier; this.apiTemplateDataSupplier = apiTemplateDataSupplier;
this.examConfigurationValueService = examConfigurationValueService; this.examConfigurationValueService = examConfigurationValueService;
this.cryptor = cryptor;
this.lmsSetupId = apiTemplateDataSupplier.getLmsSetup().id; this.lmsSetupId = apiTemplateDataSupplier.getLmsSetup().id;
} }
@ -357,8 +350,8 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
final RestrictionDataPost post = new RestrictionDataPost(); final RestrictionDataPost post = new RestrictionDataPost();
post.browserExamKeys = new ArrayList<>(restriction.browserExamKeys); post.browserExamKeys = new ArrayList<>(restriction.browserExamKeys);
post.configKeys = new ArrayList<>(restriction.configKeys); post.configKeys = new ArrayList<>(restriction.configKeys);
post.quitLink = this.getQuitLink(restriction.examId); post.quitLink = this.examConfigurationValueService.getQuitLink(restriction.examId);
post.quitSecret = this.getQuitSecret(restriction.examId); post.quitSecret = this.examConfigurationValueService.getQuitSecret(restriction.examId);
final RestrictionData r = final RestrictionData r =
this.apiPost(restTemplate, url, post, RestrictionDataPost.class, RestrictionData.class); this.apiPost(restTemplate, url, post, RestrictionDataPost.class, RestrictionData.class);
return new SEBRestriction(Long.valueOf(id), r.configKeys, r.browserExamKeys, new HashMap<String, String>()); return new SEBRestriction(Long.valueOf(id), r.configKeys, r.browserExamKeys, new HashMap<String, String>());
@ -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;
}
}
} }

View file

@ -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.client.ClientCredentialService;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; 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.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamConfigurationValueService; 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.APITemplateDataSupplier;
@ -44,7 +43,6 @@ public class OlatLmsAPITemplateFactory implements LmsAPITemplateFactory {
private final AsyncService asyncService; private final AsyncService asyncService;
private final Environment environment; private final Environment environment;
private final CacheManager cacheManager; private final CacheManager cacheManager;
private final Cryptor cryptor;
public OlatLmsAPITemplateFactory( public OlatLmsAPITemplateFactory(
final ClientHttpRequestFactoryService clientHttpRequestFactoryService, final ClientHttpRequestFactoryService clientHttpRequestFactoryService,
@ -52,8 +50,7 @@ public class OlatLmsAPITemplateFactory implements LmsAPITemplateFactory {
final ExamConfigurationValueService examConfigurationValueService, final ExamConfigurationValueService examConfigurationValueService,
final AsyncService asyncService, final AsyncService asyncService,
final Environment environment, final Environment environment,
final CacheManager cacheManager, final CacheManager cacheManager) {
final Cryptor cryptor) {
this.clientHttpRequestFactoryService = clientHttpRequestFactoryService; this.clientHttpRequestFactoryService = clientHttpRequestFactoryService;
this.clientCredentialService = clientCredentialService; this.clientCredentialService = clientCredentialService;
@ -61,7 +58,6 @@ public class OlatLmsAPITemplateFactory implements LmsAPITemplateFactory {
this.asyncService = asyncService; this.asyncService = asyncService;
this.environment = environment; this.environment = environment;
this.cacheManager = cacheManager; this.cacheManager = cacheManager;
this.cryptor = cryptor;
} }
@Override @Override
@ -72,13 +68,14 @@ public class OlatLmsAPITemplateFactory implements LmsAPITemplateFactory {
@Override @Override
public Result<LmsAPITemplate> create(final APITemplateDataSupplier apiTemplateDataSupplier) { public Result<LmsAPITemplate> create(final APITemplateDataSupplier apiTemplateDataSupplier) {
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
final OlatLmsAPITemplate olatLmsAPITemplate = new OlatLmsAPITemplate( final OlatLmsAPITemplate olatLmsAPITemplate = new OlatLmsAPITemplate(
this.clientHttpRequestFactoryService, this.clientHttpRequestFactoryService,
this.clientCredentialService, this.clientCredentialService,
apiTemplateDataSupplier, apiTemplateDataSupplier,
this.examConfigurationValueService, this.examConfigurationValueService,
this.cryptor,
this.cacheManager); this.cacheManager);
return new LmsAPITemplateAdapter( return new LmsAPITemplateAdapter(
this.asyncService, this.asyncService,
this.environment, this.environment,

View file

@ -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.LmsAPIService;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate; 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.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.ExamFinishedEvent;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamResetEvent; import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamResetEvent;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamStartedEvent; import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamStartedEvent;
@ -402,7 +402,7 @@ class ExamUpdateHandler {
log.debug("Found formerName quiz name: {}", exam.name); log.debug("Found formerName quiz name: {}", exam.name);
// get the course name identifier // get the course name identifier
final String shortname = MoodleCourseAccess.getShortname(quizId); final String shortname = MoodleUtils.getShortname(quizId);
if (StringUtils.isNotBlank(shortname)) { if (StringUtils.isNotBlank(shortname)) {
log.debug("Using short-name: {} for recovering", shortname); log.debug("Using short-name: {} for recovering", shortname);
@ -412,7 +412,7 @@ class ExamUpdateHandler {
.getOrThrow() .getOrThrow()
.stream() .stream()
.filter(quiz -> { .filter(quiz -> {
final String qShortName = MoodleCourseAccess.getShortname(quiz.id); final String qShortName = MoodleUtils.getShortname(quiz.id);
return qShortName != null && qShortName.equals(shortname); return qShortName != null && qShortName.equals(shortname);
}) })
.filter(quiz -> exam.name.equals(quiz.name)) .filter(quiz -> exam.name.equals(quiz.name))

View file

@ -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.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.LmsSetupDAO; 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.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.ExamSessionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientConnectionService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientConnectionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientSessionService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientSessionService;
@ -479,7 +480,7 @@ public class ExamAPI_V1_Controller {
return; return;
} }
this.examSessionService.getRunningExam(examId) 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)); .onSuccess(bek -> response.setHeader(API.EXAM_API_EXAM_ALT_BEK, bek));
} }

View file

@ -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.Exam.ExamType;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction; import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; 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.exam.ExamConfigurationValueService;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.olat.OlatLmsAPITemplate; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.olat.OlatLmsAPITemplate;
@ -42,8 +41,6 @@ public class OlatLmsAPITemplateTest extends AdministrationAPIIntegrationTester {
@Autowired @Autowired
private ExamConfigurationValueService examConfigurationValueService; private ExamConfigurationValueService examConfigurationValueService;
@Autowired @Autowired
private Cryptor cryptor;
@Autowired
private CacheManager cacheManager; private CacheManager cacheManager;
@Test @Test
@ -59,7 +56,6 @@ public class OlatLmsAPITemplateTest extends AdministrationAPIIntegrationTester {
null, null,
apiTemplateDataSupplier, apiTemplateDataSupplier,
this.examConfigurationValueService, this.examConfigurationValueService,
this.cryptor,
this.cacheManager); this.cacheManager);
Mockito.when(restTemplateMock.exchange(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.when(restTemplateMock.exchange(Mockito.any(), Mockito.any(), Mockito.any(),

View file

@ -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.institution.LmsSetupTestResult.ErrorType;
import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails; import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactoryImpl;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory.MoodleAPIRestTemplateImpl; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactoryImpl.MoodleAPIRestTemplateImpl;
public class MoodleCourseAccessTest { public class MoodleCourseAccessTest {
@ -42,7 +42,7 @@ public class MoodleCourseAccessTest {
@Test @Test
public void testGetExamineeAccountDetails() { public void testGetExamineeAccountDetails() {
final MoodleRestTemplateFactory moodleRestTemplateFactory = mock(MoodleRestTemplateFactory.class); final MoodleRestTemplateFactoryImpl moodleRestTemplateFactory = mock(MoodleRestTemplateFactoryImpl.class);
final MoodleAPIRestTemplateImpl moodleAPIRestTemplate = mock(MoodleAPIRestTemplateImpl.class); final MoodleAPIRestTemplateImpl moodleAPIRestTemplate = mock(MoodleAPIRestTemplateImpl.class);
when(moodleRestTemplateFactory.createRestTemplate()).thenReturn(Result.of(moodleAPIRestTemplate)); when(moodleRestTemplateFactory.createRestTemplate()).thenReturn(Result.of(moodleAPIRestTemplate));
when(moodleAPIRestTemplate.callMoodleAPIFunction( when(moodleAPIRestTemplate.callMoodleAPIFunction(
@ -118,7 +118,7 @@ public class MoodleCourseAccessTest {
@Test @Test
public void testInitAPIAccessError1() { public void testInitAPIAccessError1() {
final MoodleRestTemplateFactory moodleRestTemplateFactory = mock(MoodleRestTemplateFactory.class); final MoodleRestTemplateFactoryImpl moodleRestTemplateFactory = mock(MoodleRestTemplateFactoryImpl.class);
when(moodleRestTemplateFactory.createRestTemplate()).thenReturn(Result.ofRuntimeError("Error1")); when(moodleRestTemplateFactory.createRestTemplate()).thenReturn(Result.ofRuntimeError("Error1"));
when(moodleRestTemplateFactory.test()).thenReturn(LmsSetupTestResult.ofOkay(LmsType.MOODLE)); when(moodleRestTemplateFactory.test()).thenReturn(LmsSetupTestResult.ofOkay(LmsType.MOODLE));
@ -138,7 +138,7 @@ public class MoodleCourseAccessTest {
@Test @Test
public void testInitAPIAccessError2() { public void testInitAPIAccessError2() {
final MoodleRestTemplateFactory moodleRestTemplateFactory = mock(MoodleRestTemplateFactory.class); final MoodleRestTemplateFactoryImpl moodleRestTemplateFactory = mock(MoodleRestTemplateFactoryImpl.class);
final MoodleAPIRestTemplateImpl moodleAPIRestTemplate = mock(MoodleAPIRestTemplateImpl.class); final MoodleAPIRestTemplateImpl moodleAPIRestTemplate = mock(MoodleAPIRestTemplateImpl.class);
when(moodleRestTemplateFactory.createRestTemplate()).thenReturn(Result.of(moodleAPIRestTemplate)); when(moodleRestTemplateFactory.createRestTemplate()).thenReturn(Result.of(moodleAPIRestTemplate));
doThrow(RuntimeException.class).when(moodleAPIRestTemplate).testAPIConnection(any()); doThrow(RuntimeException.class).when(moodleAPIRestTemplate).testAPIConnection(any());
@ -160,7 +160,7 @@ public class MoodleCourseAccessTest {
@Test @Test
public void testInitAPIAccessOK() { public void testInitAPIAccessOK() {
final MoodleRestTemplateFactory moodleRestTemplateFactory = mock(MoodleRestTemplateFactory.class); final MoodleRestTemplateFactoryImpl moodleRestTemplateFactory = mock(MoodleRestTemplateFactoryImpl.class);
final MoodleAPIRestTemplateImpl moodleAPIRestTemplate = mock(MoodleAPIRestTemplateImpl.class); final MoodleAPIRestTemplateImpl moodleAPIRestTemplate = mock(MoodleAPIRestTemplateImpl.class);
when(moodleRestTemplateFactory.createRestTemplate()).thenReturn(Result.of(moodleAPIRestTemplate)); when(moodleRestTemplateFactory.createRestTemplate()).thenReturn(Result.of(moodleAPIRestTemplate));
when(moodleRestTemplateFactory.test()).thenReturn(LmsSetupTestResult.ofOkay(LmsType.MOODLE)); when(moodleRestTemplateFactory.test()).thenReturn(LmsSetupTestResult.ofOkay(LmsType.MOODLE));