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";
/** This attribute name is used to store the per exam generated app-signature-key encryption salt */
public static final String ADDITIONAL_ATTR_SIGNATURE_KEY_SALT = "SIGNATURE_KEY_SALT";
/** This attribute name is used to store the per Moolde(plugin) exam generated alternative BEK */
public static final String ADDITIONAL_ATTR_ALTERNATIVE_SEB_BEK = "ALTERNATIVE_SEB_BEK";
public enum ExamStatus {
UP_COMING,

View file

@ -11,6 +11,7 @@ package ch.ethz.seb.sebserver.gbl.model.exam;
import java.util.Collections;
import java.util.Comparator;
import java.util.Map;
import java.util.Objects;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
@ -211,6 +212,23 @@ public final class QuizData implements GrantEntity {
return this.additionalAttributes;
}
@Override
public int hashCode() {
return Objects.hash(this.id, this.lmsSetupId);
}
@Override
public boolean equals(final Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
final QuizData other = (QuizData) obj;
return Objects.equals(this.id, other.id) && Objects.equals(this.lmsSetupId, other.lmsSetupId);
}
@Override
public String toString() {
final StringBuilder builder = new StringBuilder();

View file

@ -10,6 +10,9 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.exam;
public interface ExamConfigurationValueService {
public static final String CONFIG_ATTR_NAME_QUIT_LINK = "quitURL";
public static final String CONFIG_ATTR_NAME_QUIT_SECRET = "hashedQuitPassword";
/** Get the actual SEB settings attribute value for the exam configuration mapped as default configuration
* to the given exam
*
@ -18,4 +21,8 @@ public interface ExamConfigurationValueService {
* @return The current value of the above SEB settings attribute and given exam. */
String getMappedDefaultConfigAttributeValue(Long examId, String configAttributeName);
String getQuitSecret(Long examId);
String getQuitLink(Long examId);
}

View file

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

View file

@ -8,17 +8,23 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.exam.impl;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Cryptor;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationAttributeDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationValueDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamConfigurationMapDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamConfigurationValueService;
@Lazy
@Service
@WebServiceProfile
public class ExamConfigurationValueServiceImpl implements ExamConfigurationValueService {
private static final Logger log = LoggerFactory.getLogger(ExamConfigurationValueServiceImpl.class);
@ -27,17 +33,20 @@ public class ExamConfigurationValueServiceImpl implements ExamConfigurationValue
private final ConfigurationDAO configurationDAO;
private final ConfigurationAttributeDAO configurationAttributeDAO;
private final ConfigurationValueDAO configurationValueDAO;
private final Cryptor cryptor;
public ExamConfigurationValueServiceImpl(
final ExamConfigurationMapDAO examConfigurationMapDAO,
final ConfigurationDAO configurationDAO,
final ConfigurationAttributeDAO configurationAttributeDAO,
final ConfigurationValueDAO configurationValueDAO) {
final ConfigurationValueDAO configurationValueDAO,
final Cryptor cryptor) {
this.examConfigurationMapDAO = examConfigurationMapDAO;
this.configurationDAO = configurationDAO;
this.configurationAttributeDAO = configurationAttributeDAO;
this.configurationValueDAO = configurationValueDAO;
this.cryptor = cryptor;
}
@Override
@ -66,4 +75,45 @@ public class ExamConfigurationValueServiceImpl implements ExamConfigurationValue
}
}
@Override
public String getQuitSecret(final Long examId) {
try {
final String quitSecretEncrypted = getMappedDefaultConfigAttributeValue(
examId,
CONFIG_ATTR_NAME_QUIT_SECRET);
if (StringUtils.isNotEmpty(quitSecretEncrypted)) {
try {
return this.cryptor
.decrypt(quitSecretEncrypted)
.getOrThrow()
.toString();
} catch (final Exception e) {
log.error("Failed to decrypt quitSecret: ", e);
}
}
} catch (final Exception e) {
log.error("Failed to get SEB restriction with quit secret: ", e);
}
return null;
}
@Override
public String getQuitLink(final Long examId) {
try {
return getMappedDefaultConfigAttributeValue(
examId,
CONFIG_ATTR_NAME_QUIT_LINK);
} catch (final Exception e) {
log.error("Failed to get SEB restriction with quit link: ", e);
return null;
}
}
}

View file

@ -8,8 +8,8 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.lms;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@ -113,10 +113,20 @@ public interface CourseAccessAPI {
* @return Result referencing to the Chapters model for the given course or to an error when happened. */
Result<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 {
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;
/** Indicates if the fetch is been canceled. Set this to true to cancel the asynchronous process */
public boolean canceled = false;
/** Reference to an error when the asynchronous fetch stopped with an error */
public Exception error = null;
public void finish() {

View file

@ -18,6 +18,9 @@ public interface SEBRestrictionService {
String SEB_RESTRICTION_ADDITIONAL_PROPERTY_NAME_PREFIX = "sebRestrictionProp_";
String SEB_RESTRICTION_ADDITIONAL_PROPERTY_CONFIG_KEY = "config_key";
/** This attribute name is used to store the per Moolde(plugin) exam generated alternative BEK */
String ADDITIONAL_ATTR_ALTERNATIVE_SEB_BEK =
SEB_RESTRICTION_ADDITIONAL_PROPERTY_NAME_PREFIX + "ALTERNATIVE_SEB_BEK";
/** Get the LmsAPIService that is used by the SEBRestrictionService */
LmsAPIService getLmsAPIService();

View file

@ -98,9 +98,14 @@ public abstract class AbstractCachedCourseAccess {
/** Put all QuizData to short time cache.
*
* @param quizData Collection of QuizData */
protected void putToCache(final Collection<QuizData> quizData) {
* @param quizData Collection of 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));
return quizData;
}
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)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import ch.ethz.seb.sebserver.ClientHttpRequestFactoryService;
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
import ch.ethz.seb.sebserver.gbl.client.ClientCredentialService;
import ch.ethz.seb.sebserver.gbl.client.ClientCredentials;
import ch.ethz.seb.sebserver.gbl.client.ProxyData;
import ch.ethz.seb.sebserver.gbl.model.Domain.LMS_SETUP;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier;
public class MoodleRestTemplateFactory {
private static final Logger log = LoggerFactory.getLogger(MoodleRestTemplateFactory.class);
public final JSONMapper jsonMapper;
public final APITemplateDataSupplier apiTemplateDataSupplier;
public final ClientHttpRequestFactoryService clientHttpRequestFactoryService;
public final ClientCredentialService clientCredentialService;
public final Set<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;
}
}
}
/*
* Copyright (c) 2022 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle;
import java.util.Set;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier;
public interface MoodleRestTemplateFactory {
LmsSetupTestResult test();
APITemplateDataSupplier getApiTemplateDataSupplier();
Set<String> getKnownTokenAccessPaths();
Result<MoodleAPIRestTemplate> createRestTemplate();
Result<MoodleAPIRestTemplate> createRestTemplate(final String accessTokenPath);
}

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

View file

@ -8,408 +8,34 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.legacy;
import java.util.ArrayList;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.type.TypeReference;
import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.MoodleSEBRestriction;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionAPI;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.NoSEBRestrictionException;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleAPIRestTemplate;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory;
/** GET:
* http://yourmoodle.org/webservice/rest/server.php?wstoken={token}&moodlewsrestformat=json&wsfunction=seb_restriction&courseId=123
*
* Response (JSON):
*
* <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 */
/** Dummy Implementation */
public class MoodleCourseRestriction implements SEBRestrictionAPI {
private static final Logger log = LoggerFactory.getLogger(MoodleCourseRestriction.class);
private static final String MOODLE_DEFAULT_COURSE_RESTRICTION_WS_FUNCTION = "seb_restriction";
private static final String MOODLE_DEFAULT_COURSE_RESTRICTION_WS_FUNCTION_CREATE = "seb_restriction_create";
private static final String MOODLE_DEFAULT_COURSE_RESTRICTION_WS_FUNCTION_UPDATE = "seb_restriction_update";
private static final String MOODLE_DEFAULT_COURSE_RESTRICTION_WS_FUNCTION_DELETE = "seb_restriction_delete";
private static final String MOODLE_DEFAULT_COURSE_RESTRICTION_SHORT_NAME = "shortname";
private static final String MOODLE_DEFAULT_COURSE_RESTRICTION_ID_NUMBER = "idnumber";
private static final String MOODLE_DEFAULT_COURSE_RESTRICTION_QUIZ_ID = "quizId";
private static final String MOODLE_DEFAULT_COURSE_RESTRICTION_CONFIG_KEY = "configKey";
private static final String MOODLE_DEFAULT_COURSE_RESTRICTION_BROWSER_KEY = "browserKey";
private final JSONMapper jsonMapper;
private final MoodleRestTemplateFactory moodleRestTemplateFactory;
private MoodleAPIRestTemplate restTemplate;
public MoodleCourseRestriction(
final JSONMapper jsonMapper,
final MoodleRestTemplateFactory moodleRestTemplateFactory) {
this.jsonMapper = jsonMapper;
this.moodleRestTemplateFactory = moodleRestTemplateFactory;
}
@Override
public LmsSetupTestResult testCourseRestrictionAPI() {
// try to call the SEB Restrictions API
try {
final MoodleAPIRestTemplate template = getRestTemplate()
.getOrThrow();
final String jsonResponse = template.callMoodleAPIFunction(
MOODLE_DEFAULT_COURSE_RESTRICTION_WS_FUNCTION,
new LinkedMultiValueMap<>(),
null);
final Error checkError = this.checkError(jsonResponse);
if (checkError != null) {
return LmsSetupTestResult.ofQuizRestrictionAPIError(LmsType.MOODLE, checkError.exception);
}
} catch (final Exception e) {
log.debug("Moodle SEB restriction API not available: ", e);
return LmsSetupTestResult.ofQuizRestrictionAPIError(LmsType.MOODLE, e.getMessage());
}
return LmsSetupTestResult.ofOkay(LmsType.MOODLE);
return LmsSetupTestResult.ofQuizAccessAPIError(LmsType.MOODLE, "SEB restriction not supported");
}
@Override
public Result<SEBRestriction> getSEBClientRestriction(final Exam exam) {
return Result.tryCatch(() -> {
return getSEBRestriction(
MoodleCourseAccess.getQuizId(exam.externalId),
MoodleCourseAccess.getShortname(exam.externalId),
MoodleCourseAccess.getIdnumber(exam.externalId))
.map(restriction -> SEBRestriction.from(exam.id, restriction))
.getOrThrow();
});
return Result.ofError(new UnsupportedOperationException("SEB restriction not supported"));
}
@Override
public Result<SEBRestriction> applySEBClientRestriction(
final Exam exam,
final SEBRestriction sebRestrictionData) {
return this.updateSEBRestriction(
exam.externalId,
MoodleSEBRestriction.from(sebRestrictionData))
.map(result -> sebRestrictionData);
public Result<SEBRestriction> applySEBClientRestriction(final Exam exam, final SEBRestriction sebRestrictionData) {
return Result.ofError(new UnsupportedOperationException("SEB restriction not supported"));
}
@Override
public Result<Exam> releaseSEBClientRestriction(final Exam exam) {
return this.deleteSEBRestriction(exam.externalId)
.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();
}
return Result.ofError(new UnsupportedOperationException("SEB restriction not supported"));
}
}

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

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;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.CacheManager;
import org.springframework.core.env.Environment;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonMappingException;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
import ch.ethz.seb.sebserver.gbl.async.AsyncService;
import ch.ethz.seb.sebserver.gbl.async.CircuitBreaker;
import ch.ethz.seb.sebserver.gbl.model.exam.Chapters;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.CourseAccessAPI;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.AbstractCachedCourseAccess;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleAPIRestTemplate;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleAPIRestTemplate.Warning;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils.CourseData;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils.CourseQuizData;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils.Courses;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils.MoodlePluginUserDetails;
public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess implements CourseAccessAPI {
private static final Logger log = LoggerFactory.getLogger(MoodlePluginCourseAccess.class);
static final String COURSES_API_FUNCTION_NAME = "local_sebserver_get_courses";
static final String QUIZZES_BY_COURSES_API_FUNCTION_NAME = "local_sebserver_get_quizzes_by_courses";
static final String USERS_API_FUNCTION_NAME = "local_sebserver_get_users";
public static final String MOODLE_QUIZ_START_URL_PATH = "mod/quiz/view.php?id=";
public static final String COURSES_API_FUNCTION_NAME = "quizaccess_sebserver_get_courses";
public static final String QUIZZES_BY_COURSES_API_FUNCTION_NAME = "quizaccess_sebserver_get_quizzes_by_courses";
public static final String USERS_API_FUNCTION_NAME = "quizaccess_sebserver_get_users";
static final String CRITERIA_FROM_DATE = "from_date";
static final String CRITERIA_TO_DATE = "to_date";
static final String CRITERIA_LIMIT_FROM = "limitfrom";
static final String CRITERIA_LIMIT_NUM = "limitnum";
public static final String ATTR_FIELD = "field";
public static final String CRITERIA_COURSE_IDS = "ids";
public static final String CRITERIA_FROM_DATE = "from_date";
public static final String CRITERIA_TO_DATE = "to_date";
public static final String CRITERIA_LIMIT_FROM = "limitfrom";
public static final String CRITERIA_LIMIT_NUM = "limitnum";
private final JSONMapper jsonMapper;
private final MoodleRestTemplateFactory moodleRestTemplateFactory;
private final MoodleRestTemplateFactory restTemplateFactory;
private final CircuitBreaker<String> protectedMoodlePageCall;
private final boolean prependShortCourseName;
private final int pageSize;
private final int maxSize;
private MoodleAPIRestTemplate restTemplate;
public MoodlePluginCourseAccess(
final JSONMapper jsonMapper,
final MoodleRestTemplateFactory moodleRestTemplateFactory,
final CacheManager cacheManager) {
final AsyncService asyncService,
final MoodleRestTemplateFactory restTemplateFactory,
final CacheManager cacheManager,
final Environment environment) {
super(cacheManager);
this.jsonMapper = jsonMapper;
this.moodleRestTemplateFactory = moodleRestTemplateFactory;
this.restTemplateFactory = restTemplateFactory;
this.prependShortCourseName = BooleanUtils.toBoolean(environment.getProperty(
"sebserver.webservice.lms.moodle.prependShortCourseName",
Constants.TRUE_STRING));
this.protectedMoodlePageCall = asyncService.createCircuitBreaker(
environment.getProperty(
"sebserver.webservice.circuitbreaker.moodleRestCall.attempts",
Integer.class,
2),
environment.getProperty(
"sebserver.webservice.circuitbreaker.moodleRestCall.blockingTime",
Long.class,
Constants.SECOND_IN_MILLIS * 20),
environment.getProperty(
"sebserver.webservice.circuitbreaker.moodleRestCall.timeToRecover",
Long.class,
Constants.MINUTE_IN_MILLIS));
this.maxSize =
environment.getProperty("sebserver.webservice.cache.moodle.course.maxSize", Integer.class, 10000);
this.pageSize =
environment.getProperty("sebserver.webservice.cache.moodle.course.pageSize", Integer.class, 10);
}
@Override
protected Long getLmsSetupId() {
return this.restTemplateFactory.getApiTemplateDataSupplier().getLmsSetup().id;
}
@Override
public LmsSetupTestResult testCourseAccessAPI() {
final LmsSetupTestResult attributesCheck = this.moodleRestTemplateFactory.test();
final LmsSetupTestResult attributesCheck = this.restTemplateFactory.test();
if (!attributesCheck.isOk()) {
return attributesCheck;
}
@ -73,78 +136,417 @@ public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess impleme
final Result<MoodleAPIRestTemplate> restTemplateRequest = getRestTemplate();
if (restTemplateRequest.hasError()) {
final String message = "Failed to gain access token from Moodle Rest API:\n tried token endpoints: " +
this.moodleRestTemplateFactory.knownTokenAccessPaths;
this.restTemplateFactory.getKnownTokenAccessPaths();
log.error(message + " cause: {}", restTemplateRequest.getError().getMessage());
return LmsSetupTestResult.ofTokenRequestError(LmsType.MOODLE_PLUGIN, message);
}
final MoodleAPIRestTemplate restTemplate = restTemplateRequest.get();
// try {
// restTemplate.testAPIConnection(
// COURSES_API_FUNCTION_NAME,
// QUIZZES_BY_COURSES_API_FUNCTION_NAME,
// USERS_API_FUNCTION_NAME);
// } catch (final RuntimeException e) {
// log.error("Failed to access Moodle course API: ", e);
// return LmsSetupTestResult.ofQuizAccessAPIError(LmsType.MOODLE_PLUGIN, e.getMessage());
// }
try {
restTemplate.testAPIConnection(
COURSES_API_FUNCTION_NAME,
QUIZZES_BY_COURSES_API_FUNCTION_NAME,
USERS_API_FUNCTION_NAME);
} catch (final RuntimeException e) {
log.error("Failed to access Moodle course API: ", e);
return LmsSetupTestResult.ofQuizAccessAPIError(LmsType.MOODLE_PLUGIN, e.getMessage());
}
return LmsSetupTestResult.ofOkay(LmsType.MOODLE_PLUGIN);
}
@Override
public Result<List<QuizData>> getQuizzes(final FilterMap filterMap) {
System.out.println("***************** filterMap: " + filterMap);
// 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;
return Result.ofError(new UnsupportedOperationException());
}
@Override
public void fetchQuizzes(final FilterMap filterMap, final AsyncQuizFetchBuffer asyncQuizFetchBuffer) {
// TODO Auto-generated method stub
try {
int page = 0;
int failedAttempts = 0;
final DateTime quizFromTime = filterMap.getQuizFromTime();
final Predicate<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
public Result<QuizData> getQuiz(final String id) {
// TODO Auto-generated method stub
return null;
return Result.tryCatch(() -> {
final QuizData fromCache = super.getFromCache(id);
if (fromCache != null) {
return fromCache;
}
final Set<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
public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeUserId) {
// TODO Auto-generated method stub
return null;
return Result.tryCatch(() -> {
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
public String getExamineeName(final String examineeUserId) {
// TODO Auto-generated method stub
return null;
return getExamineeAccountDetails(examineeUserId)
.map(ExamineeAccountDetails::getDisplayName)
.onError(error -> log.warn("Failed to request user-name for ID: {}", error.getMessage(), error))
.getOr(examineeUserId);
}
@Override
public Result<Chapters> getCourseChapters(final String courseId) {
// TODO Auto-generated method stub
return null;
return Result.of(new Chapters(Collections.emptyList()));
}
@Override
protected Long getLmsSetupId() {
// TODO Auto-generated method stub
return null;
private String getLmsSetupName() {
return this.restTemplateFactory.getApiTemplateDataSupplier().getLmsSetup().name;
}
private void fetchQuizzesPage(
final int page,
final DateTime quizFromTime,
final AsyncQuizFetchBuffer asyncQuizFetchBuffer,
final Predicate<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() {
if (this.restTemplate == null) {
final Result<MoodleAPIRestTemplate> templateRequest = this.moodleRestTemplateFactory
final Result<MoodleAPIRestTemplate> templateRequest = this.restTemplateFactory
.createRestTemplate();
if (templateRequest.hasError()) {
return templateRequest;
@ -156,99 +558,4 @@ public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess impleme
return Result.of(this.restTemplate);
}
// ---- Mapping Classes ---
@JsonIgnoreProperties(ignoreUnknown = true)
private static final class Courses {
final Collection<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;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.LinkedMultiValueMap;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamConfigurationValueService;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionAPI;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleAPIRestTemplate;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils.MoodleQuizRestriction;
public class MoodlePluginCourseRestriction implements SEBRestrictionAPI {
private static final Logger log = LoggerFactory.getLogger(MoodlePluginCourseRestriction.class);
public static final String RESTRICTION_GET_FUNCTION_NAME = "quizaccess_sebserver_get_restriction";
public static final String RESTRICTION_SET_FUNCTION_NAME = "quizaccess_sebserver_set_restriction";
public static final String ATTRIBUTE_QUIZ_ID = "quiz_id";
public static final String ATTRIBUTE_CONFIG_KEYS = "config_keys";
public static final String ATTRIBUTE_BROWSER_EXAM_KEYS = "browser_exam_keys";
public static final String ATTRIBUTE_QUIT_URL = "quit_link";
public static final String ATTRIBUTE_QUIT_SECRET = "quit_secret";
private final JSONMapper jsonMapper;
private final MoodleRestTemplateFactory restTemplateFactory;
private final ExamConfigurationValueService examConfigurationValueService;
private MoodleAPIRestTemplate restTemplate;
public MoodlePluginCourseRestriction(
final JSONMapper jsonMapper,
final MoodleRestTemplateFactory restTemplateFactory,
final ExamConfigurationValueService examConfigurationValueService) {
this.jsonMapper = jsonMapper;
this.restTemplateFactory = restTemplateFactory;
this.examConfigurationValueService = examConfigurationValueService;
}
@Override
public LmsSetupTestResult testCourseRestrictionAPI() {
// TODO Auto-generated method stub
final LmsSetupTestResult attributesCheck = this.restTemplateFactory.test();
if (!attributesCheck.isOk()) {
return attributesCheck;
}
final Result<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);
}
@Override
public Result<SEBRestriction> getSEBClientRestriction(final Exam exam) {
// TODO Auto-generated method stub
return null;
return getRestTemplate().map(restTemplate -> {
if (log.isDebugEnabled()) {
log.debug("Get SEB Client restriction on exam: {}", exam);
}
final String quizId = MoodleUtils.getQuizId(exam.getExternalId());
final LinkedMultiValueMap<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
@ -34,14 +125,113 @@ public class MoodlePluginCourseRestriction implements SEBRestrictionAPI {
final Exam exam,
final SEBRestriction sebRestrictionData) {
// TODO Auto-generated method stub
return null;
return Result.tryCatch(() -> {
if (log.isDebugEnabled()) {
log.debug("Apply SEB Client restriction on exam: {}", exam);
}
final String quizId = MoodleUtils.getQuizId(exam.getExternalId());
final LinkedMultiValueMap<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
public Result<Exam> releaseSEBClientRestriction(final Exam exam) {
// TODO Auto-generated method stub
return null;
return Result.tryCatch(() -> {
if (log.isDebugEnabled()) {
log.debug("Release SEB Client restriction on exam: {}", exam);
}
final String quizId = MoodleUtils.getQuizId(exam.getExternalId());
final LinkedMultiValueMap<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.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamConfigurationValueService;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplateFactory;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.LmsAPITemplateAdapter;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MockupRestTemplateFactory;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodlePluginCheck;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory;
@Lazy
@ -41,6 +44,7 @@ public class MooldePluginLmsAPITemplateFactory implements LmsAPITemplateFactory
private final AsyncService asyncService;
private final Environment environment;
private final ClientCredentialService clientCredentialService;
private final ExamConfigurationValueService examConfigurationValueService;
private final ClientHttpRequestFactoryService clientHttpRequestFactoryService;
private final ApplicationContext applicationContext;
private final String[] alternativeTokenRequestPaths;
@ -52,6 +56,7 @@ public class MooldePluginLmsAPITemplateFactory implements LmsAPITemplateFactory
final AsyncService asyncService,
final Environment environment,
final ClientCredentialService clientCredentialService,
final ExamConfigurationValueService examConfigurationValueService,
final ClientHttpRequestFactoryService clientHttpRequestFactoryService,
final ApplicationContext applicationContext,
@Value("${sebserver.webservice.lms.moodle.api.token.request.paths:}") final String alternativeTokenRequestPaths) {
@ -62,6 +67,7 @@ public class MooldePluginLmsAPITemplateFactory implements LmsAPITemplateFactory
this.asyncService = asyncService;
this.environment = environment;
this.clientCredentialService = clientCredentialService;
this.examConfigurationValueService = examConfigurationValueService;
this.clientHttpRequestFactoryService = clientHttpRequestFactoryService;
this.applicationContext = applicationContext;
this.alternativeTokenRequestPaths = (alternativeTokenRequestPaths != null)
@ -78,19 +84,27 @@ public class MooldePluginLmsAPITemplateFactory implements LmsAPITemplateFactory
public Result<LmsAPITemplate> create(final APITemplateDataSupplier apiTemplateDataSupplier) {
return Result.tryCatch(() -> {
final MoodleRestTemplateFactory moodleRestTemplateFactory = new MoodleRestTemplateFactory(
this.jsonMapper,
apiTemplateDataSupplier,
this.clientCredentialService,
this.clientHttpRequestFactoryService,
this.alternativeTokenRequestPaths);
// final MoodleRestTemplateFactory moodleRestTemplateFactory = new MoodleRestTemplateFactoryImpl(
// this.jsonMapper,
// apiTemplateDataSupplier,
// this.clientCredentialService,
// this.clientHttpRequestFactoryService,
// this.alternativeTokenRequestPaths);
final MoodleRestTemplateFactory moodleRestTemplateFactory =
new MockupRestTemplateFactory(apiTemplateDataSupplier);
final MoodlePluginCourseAccess moodlePluginCourseAccess = new MoodlePluginCourseAccess(
this.jsonMapper,
this.asyncService,
moodleRestTemplateFactory,
this.cacheManager);
this.cacheManager,
this.environment);
final MoodlePluginCourseRestriction moodlePluginCourseRestriction = new MoodlePluginCourseRestriction();
final MoodlePluginCourseRestriction moodlePluginCourseRestriction = new MoodlePluginCourseRestriction(
this.jsonMapper,
moodleRestTemplateFactory,
this.examConfigurationValueService);
return new LmsAPITemplateAdapter(
this.asyncService,

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.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails;
import ch.ethz.seb.sebserver.gbl.util.Cryptor;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
@ -68,14 +67,10 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
private static final String ADDITIONAL_ATTR_QUIT_LINK = "ADDITIONAL_ATTR_QUIT_LINK";
private static final String ADDITIONAL_ATTR_QUIT_SECRET = "ADDITIONAL_ATTR_QUIT_SECRET";
private static final String CONFIG_ATTR_NAME_QUIT_LINK = "quitURL";
private static final String CONFIG_ATTR_NAME_QUIT_SECRET = "hashedQuitPassword";
private final ClientHttpRequestFactoryService clientHttpRequestFactoryService;
private final ClientCredentialService clientCredentialService;
private final APITemplateDataSupplier apiTemplateDataSupplier;
private final ExamConfigurationValueService examConfigurationValueService;
private final Cryptor cryptor;
private final Long lmsSetupId;
private OlatLmsRestTemplate cachedRestTemplate;
@ -85,7 +80,6 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
final ClientCredentialService clientCredentialService,
final APITemplateDataSupplier apiTemplateDataSupplier,
final ExamConfigurationValueService examConfigurationValueService,
final Cryptor cryptor,
final CacheManager cacheManager) {
super(cacheManager);
@ -94,7 +88,6 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
this.clientCredentialService = clientCredentialService;
this.apiTemplateDataSupplier = apiTemplateDataSupplier;
this.examConfigurationValueService = examConfigurationValueService;
this.cryptor = cryptor;
this.lmsSetupId = apiTemplateDataSupplier.getLmsSetup().id;
}
@ -357,8 +350,8 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
final RestrictionDataPost post = new RestrictionDataPost();
post.browserExamKeys = new ArrayList<>(restriction.browserExamKeys);
post.configKeys = new ArrayList<>(restriction.configKeys);
post.quitLink = this.getQuitLink(restriction.examId);
post.quitSecret = this.getQuitSecret(restriction.examId);
post.quitLink = this.examConfigurationValueService.getQuitLink(restriction.examId);
post.quitSecret = this.examConfigurationValueService.getQuitSecret(restriction.examId);
final RestrictionData r =
this.apiPost(restTemplate, url, post, RestrictionDataPost.class, RestrictionData.class);
return new SEBRestriction(Long.valueOf(id), r.configKeys, r.browserExamKeys, new HashMap<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.model.institution.LmsSetup.LmsType;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Cryptor;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamConfigurationValueService;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier;
@ -44,7 +43,6 @@ public class OlatLmsAPITemplateFactory implements LmsAPITemplateFactory {
private final AsyncService asyncService;
private final Environment environment;
private final CacheManager cacheManager;
private final Cryptor cryptor;
public OlatLmsAPITemplateFactory(
final ClientHttpRequestFactoryService clientHttpRequestFactoryService,
@ -52,8 +50,7 @@ public class OlatLmsAPITemplateFactory implements LmsAPITemplateFactory {
final ExamConfigurationValueService examConfigurationValueService,
final AsyncService asyncService,
final Environment environment,
final CacheManager cacheManager,
final Cryptor cryptor) {
final CacheManager cacheManager) {
this.clientHttpRequestFactoryService = clientHttpRequestFactoryService;
this.clientCredentialService = clientCredentialService;
@ -61,7 +58,6 @@ public class OlatLmsAPITemplateFactory implements LmsAPITemplateFactory {
this.asyncService = asyncService;
this.environment = environment;
this.cacheManager = cacheManager;
this.cryptor = cryptor;
}
@Override
@ -72,13 +68,14 @@ public class OlatLmsAPITemplateFactory implements LmsAPITemplateFactory {
@Override
public Result<LmsAPITemplate> create(final APITemplateDataSupplier apiTemplateDataSupplier) {
return Result.tryCatch(() -> {
final OlatLmsAPITemplate olatLmsAPITemplate = new OlatLmsAPITemplate(
this.clientHttpRequestFactoryService,
this.clientCredentialService,
apiTemplateDataSupplier,
this.examConfigurationValueService,
this.cryptor,
this.cacheManager);
return new LmsAPITemplateAdapter(
this.asyncService,
this.environment,

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

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

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

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