diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/api/POSTMapper.java b/src/main/java/ch/ethz/seb/sebserver/gbl/api/POSTMapper.java index aabeeb7b..4e526a2a 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/api/POSTMapper.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/api/POSTMapper.java @@ -270,4 +270,14 @@ public class POSTMapper { this.params.putIfAbsent(name, Arrays.asList(value)); return (T) this; } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append("POSTMapper [params="); + builder.append(this.params); + builder.append("]"); + return builder.toString(); + } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/LmsSetup.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/LmsSetup.java index d82845fc..9fb6852e 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/LmsSetup.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/LmsSetup.java @@ -60,6 +60,8 @@ public final class LmsSetup implements GrantEntity, Activatable { OPEN_EDX(Features.COURSE_API, Features.SEB_RESTRICTION), /** The Moodle binding features only the course access API so far */ MOODLE(Features.COURSE_API /* , Features.SEB_RESTRICTION */), + /** The Moodle binding features with SEB Server integration plugin for fully featured */ + MOODLE_PLUGIN(Features.COURSE_API, Features.SEB_RESTRICTION), /** The Ans Delft binding is on the way */ ANS_DELFT(Features.COURSE_API, Features.SEB_RESTRICTION), /** The OpenOLAT binding is on the way */ diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleAPIRestTemplate.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleAPIRestTemplate.java new file mode 100644 index 00000000..6ec3934d --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleAPIRestTemplate.java @@ -0,0 +1,87 @@ +/* + * 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.springframework.util.MultiValueMap; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +public interface MoodleAPIRestTemplate { + + String URI_VAR_USER_NAME = "username"; + String URI_VAR_PASSWORD = "pwd"; + String URI_VAR_SERVICE = "service"; + + String MOODLE_DEFAULT_TOKEN_REQUEST_PATH = + "/login/token.php?username={" + URI_VAR_USER_NAME + + "}&password={" + URI_VAR_PASSWORD + "}&service={" + URI_VAR_SERVICE + "}"; + + String MOODLE_DEFAULT_REST_API_PATH = "/webservice/rest/server.php"; + String REST_REQUEST_TOKEN_NAME = "wstoken"; + String REST_REQUEST_FUNCTION_NAME = "wsfunction"; + String REST_REQUEST_FORMAT_NAME = "moodlewsrestformat"; + + String getService(); + + void setService(String service); + + CharSequence getAccessToken(); + + void testAPIConnection(String... functions); + + String callMoodleAPIFunction(String functionName); + + String callMoodleAPIFunction( + final String functionName, + final MultiValueMap queryAttributes); + + String callMoodleAPIFunction( + final String functionName, + final MultiValueMap queryParams, + final MultiValueMap queryAttributes); + + /** This maps a Moodle warning JSON object */ + @JsonIgnoreProperties(ignoreUnknown = true) + static final class Warning { + final String item; + final String itemid; + final String warningcode; + final String message; + + @JsonCreator + public Warning( + @JsonProperty(value = "item") final String item, + @JsonProperty(value = "itemid") final String itemid, + @JsonProperty(value = "warningcode") final String warningcode, + @JsonProperty(value = "message") final String message) { + + this.item = item; + this.itemid = itemid; + this.warningcode = warningcode; + this.message = message; + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append("Warning [item="); + builder.append(this.item); + builder.append(", itemid="); + builder.append(this.itemid); + builder.append(", warningcode="); + builder.append(this.warningcode); + builder.append(", message="); + builder.append(this.message); + builder.append("]"); + return builder.toString(); + } + } +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/legacy/MoodleRestTemplateFactory.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleRestTemplateFactory.java similarity index 86% rename from src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/legacy/MoodleRestTemplateFactory.java rename to src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleRestTemplateFactory.java index f3b803ae..1ed0552a 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/legacy/MoodleRestTemplateFactory.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleRestTemplateFactory.java @@ -6,7 +6,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.legacy; +package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle; import java.util.ArrayList; import java.util.Arrays; @@ -57,11 +57,11 @@ public class MoodleRestTemplateFactory { private static final Logger log = LoggerFactory.getLogger(MoodleRestTemplateFactory.class); - final JSONMapper jsonMapper; - final APITemplateDataSupplier apiTemplateDataSupplier; - final ClientHttpRequestFactoryService clientHttpRequestFactoryService; - final ClientCredentialService clientCredentialService; - final Set knownTokenAccessPaths; + public final JSONMapper jsonMapper; + public final APITemplateDataSupplier apiTemplateDataSupplier; + public final ClientHttpRequestFactoryService clientHttpRequestFactoryService; + public final ClientCredentialService clientCredentialService; + public final Set knownTokenAccessPaths; public MoodleRestTemplateFactory( final JSONMapper jsonMapper, @@ -75,11 +75,12 @@ public class MoodleRestTemplateFactory { this.clientCredentialService = clientCredentialService; this.clientHttpRequestFactoryService = clientHttpRequestFactoryService; - this.knownTokenAccessPaths = new HashSet<>(); - this.knownTokenAccessPaths.add(MoodleAPIRestTemplate.MOODLE_DEFAULT_TOKEN_REQUEST_PATH); + final Set paths = new HashSet<>(); + paths.add(MoodleAPIRestTemplate.MOODLE_DEFAULT_TOKEN_REQUEST_PATH); if (alternativeTokenRequestPaths != null) { - this.knownTokenAccessPaths.addAll(Arrays.asList(alternativeTokenRequestPaths)); + paths.addAll(Arrays.asList(alternativeTokenRequestPaths)); } + this.knownTokenAccessPaths = Utils.immutableSetOf(paths); } APITemplateDataSupplier getApiTemplateDataSupplier() { @@ -125,7 +126,7 @@ public class MoodleRestTemplateFactory { return LmsSetupTestResult.ofOkay(LmsType.MOODLE); } - Result createRestTemplate() { + public Result createRestTemplate() { final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup(); @@ -149,7 +150,7 @@ public class MoodleRestTemplateFactory { ") on paths: " + this.knownTokenAccessPaths)); } - Result createRestTemplate(final String accessTokenPath) { + public Result createRestTemplate(final String accessTokenPath) { final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup(); @@ -162,8 +163,9 @@ public class MoodleRestTemplateFactory { .getPlainClientSecret(credentials) .getOrThrow(); - final MoodleAPIRestTemplate restTemplate = new MoodleAPIRestTemplate( + final MoodleAPIRestTemplateImpl restTemplate = new MoodleAPIRestTemplateImpl( this.jsonMapper, + this.apiTemplateDataSupplier, lmsSetup.lmsApiUrl, accessTokenPath, lmsSetup.lmsRestApiToken, @@ -187,22 +189,13 @@ public class MoodleRestTemplateFactory { }); } - public class MoodleAPIRestTemplate extends RestTemplate { + public static class MoodleAPIRestTemplateImpl extends RestTemplate implements MoodleAPIRestTemplate { - public static final String URI_VAR_USER_NAME = "username"; - public static final String URI_VAR_PASSWORD = "pwd"; - public static final String URI_VAR_SERVICE = "service"; - - private static final String MOODLE_DEFAULT_TOKEN_REQUEST_PATH = - "/login/token.php?username={" + URI_VAR_USER_NAME + - "}&password={" + URI_VAR_PASSWORD + "}&service={" + URI_VAR_SERVICE + "}"; - - private static final String MOODLE_DEFAULT_REST_API_PATH = "/webservice/rest/server.php"; - private static final String REST_REQUEST_TOKEN_NAME = "wstoken"; - private static final String REST_REQUEST_FUNCTION_NAME = "wsfunction"; - private static final String REST_REQUEST_FORMAT_NAME = "moodlewsrestformat"; 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; @@ -211,14 +204,18 @@ public class MoodleRestTemplateFactory { private final Map tokenReqURIVars; private final HttpEntity tokenReqEntity = new HttpEntity<>(new LinkedMultiValueMap<>()); - protected MoodleAPIRestTemplate( + 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; @@ -230,14 +227,17 @@ public class MoodleRestTemplateFactory { } + @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(); @@ -246,22 +246,27 @@ public class MoodleRestTemplateFactory { return this.accessToken; } + @Override public void testAPIConnection(final String... functions) { try { final String apiInfo = this.callMoodleAPIFunction(REST_API_TEST_FUNCTION); - final WebserviceInfo webserviceInfo = - MoodleRestTemplateFactory.this.jsonMapper.readValue(apiInfo, WebserviceInfo.class); + final WebserviceInfo webserviceInfo = this.jsonMapper.readValue( + apiInfo, + WebserviceInfo.class); if (StringUtils.isBlank(webserviceInfo.username) || StringUtils.isBlank(webserviceInfo.userid)) { throw new RuntimeException("Invalid WebserviceInfo: " + webserviceInfo); } - final List missingAPIFunctions = Arrays.stream(functions) - .filter(f -> !webserviceInfo.functions.containsKey(f)) - .collect(Collectors.toList()); + if (functions != null) { - if (!missingAPIFunctions.isEmpty()) { - throw new RuntimeException("Missing Moodle Webservice API functions: " + missingAPIFunctions); + final List missingAPIFunctions = Arrays.stream(functions) + .filter(f -> !webserviceInfo.functions.containsKey(f)) + .collect(Collectors.toList()); + + if (!missingAPIFunctions.isEmpty()) { + throw new RuntimeException("Missing Moodle Webservice API functions: " + missingAPIFunctions); + } } } catch (final RuntimeException re) { @@ -271,16 +276,19 @@ public class MoodleRestTemplateFactory { } } + @Override public String callMoodleAPIFunction(final String functionName) { return callMoodleAPIFunction(functionName, null, null); } + @Override public String callMoodleAPIFunction( final String functionName, final MultiValueMap queryAttributes) { return callMoodleAPIFunction(functionName, null, queryAttributes); } + @Override public String callMoodleAPIFunction( final String functionName, final MultiValueMap queryParams, @@ -319,9 +327,7 @@ public class MoodleRestTemplateFactory { functionReqEntity, String.class); - final LmsSetup lmsSetup = MoodleRestTemplateFactory.this.apiTemplateDataSupplier - .getLmsSetup(); - + final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup(); if (response.getStatusCode() != HttpStatus.OK) { throw new RuntimeException( "Failed to call Moodle webservice API function: " + functionName + " lms setup: " + @@ -347,9 +353,7 @@ public class MoodleRestTemplateFactory { private void requestAccessToken() { - final LmsSetup lmsSetup = MoodleRestTemplateFactory.this.apiTemplateDataSupplier - .getLmsSetup(); - + final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup(); try { final ResponseEntity response = super.exchange( @@ -369,7 +373,7 @@ public class MoodleRestTemplateFactory { } try { - final MoodleToken moodleToken = MoodleRestTemplateFactory.this.jsonMapper.readValue( + final MoodleToken moodleToken = this.jsonMapper.readValue( response.getBody(), MoodleToken.class); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/legacy/MoodleCourseAccess.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/legacy/MoodleCourseAccess.java index 91632190..44086d7d 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/legacy/MoodleCourseAccess.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/legacy/MoodleCourseAccess.java @@ -48,9 +48,11 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; 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.legacy.MoodleCourseDataAsyncLoader.CourseDataShort; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.legacy.MoodleCourseDataAsyncLoader.CourseQuizShort; -import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.legacy.MoodleRestTemplateFactory.MoodleAPIRestTemplate; /** Implements the LmsAPITemplate for Open edX LMS Course API access. * @@ -885,40 +887,4 @@ public class MoodleCourseAccess implements CourseAccessAPI { } } - @JsonIgnoreProperties(ignoreUnknown = true) - static final class Warning { - final String item; - final String itemid; - final String warningcode; - final String message; - - @JsonCreator - public Warning( - @JsonProperty(value = "item") final String item, - @JsonProperty(value = "itemid") final String itemid, - @JsonProperty(value = "warningcode") final String warningcode, - @JsonProperty(value = "message") final String message) { - - this.item = item; - this.itemid = itemid; - this.warningcode = warningcode; - this.message = message; - } - - @Override - public String toString() { - final StringBuilder builder = new StringBuilder(); - builder.append("Warning [item="); - builder.append(this.item); - builder.append(", itemid="); - builder.append(this.itemid); - builder.append(", warningcode="); - builder.append(this.warningcode); - builder.append(", message="); - builder.append(this.message); - builder.append("]"); - return builder.toString(); - } - } - } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/legacy/MoodleCourseDataAsyncLoader.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/legacy/MoodleCourseDataAsyncLoader.java index ad2a7831..b54ff140 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/legacy/MoodleCourseDataAsyncLoader.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/legacy/MoodleCourseDataAsyncLoader.java @@ -47,8 +47,8 @@ import ch.ethz.seb.sebserver.gbl.async.AsyncService; import ch.ethz.seb.sebserver.gbl.async.CircuitBreaker; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.gbl.util.Utils; -import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.legacy.MoodleCourseAccess.Warning; -import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.legacy.MoodleRestTemplateFactory.MoodleAPIRestTemplate; +import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleAPIRestTemplate; +import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleAPIRestTemplate.Warning; @Lazy @Component diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/legacy/MoodleCourseRestriction.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/legacy/MoodleCourseRestriction.java index 96008d34..67b68935 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/legacy/MoodleCourseRestriction.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/legacy/MoodleCourseRestriction.java @@ -30,7 +30,8 @@ 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.legacy.MoodleRestTemplateFactory.MoodleAPIRestTemplate; +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 diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleLmsAPITemplateFactory.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/legacy/MoodleLmsAPITemplateFactory.java similarity index 86% rename from src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleLmsAPITemplateFactory.java rename to src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/legacy/MoodleLmsAPITemplateFactory.java index 6db52389..8e55f800 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleLmsAPITemplateFactory.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/legacy/MoodleLmsAPITemplateFactory.java @@ -6,10 +6,11 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle; +package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.legacy; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Value; +import org.springframework.cache.CacheManager; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Lazy; import org.springframework.core.env.Environment; @@ -28,10 +29,7 @@ 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.legacy.MoodleCourseAccess; -import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.legacy.MoodleCourseDataAsyncLoader; -import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.legacy.MoodleCourseRestriction; -import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.legacy.MoodleRestTemplateFactory; +import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.plugin.MoodlePluginCheck; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.plugin.MoodlePluginCourseAccess; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.plugin.MoodlePluginCourseRestriction; @@ -43,6 +41,7 @@ public class MoodleLmsAPITemplateFactory implements LmsAPITemplateFactory { private final MoodlePluginCheck moodlePluginCheck; private final JSONMapper jsonMapper; + private final CacheManager cacheManager; private final AsyncService asyncService; private final Environment environment; private final ClientCredentialService clientCredentialService; @@ -53,6 +52,7 @@ public class MoodleLmsAPITemplateFactory implements LmsAPITemplateFactory { protected MoodleLmsAPITemplateFactory( final MoodlePluginCheck moodlePluginCheck, final JSONMapper jsonMapper, + final CacheManager cacheManager, final AsyncService asyncService, final Environment environment, final ClientCredentialService clientCredentialService, @@ -62,6 +62,7 @@ public class MoodleLmsAPITemplateFactory implements LmsAPITemplateFactory { this.moodlePluginCheck = moodlePluginCheck; this.jsonMapper = jsonMapper; + this.cacheManager = cacheManager; this.asyncService = asyncService; this.environment = environment; this.clientCredentialService = clientCredentialService; @@ -87,9 +88,19 @@ public class MoodleLmsAPITemplateFactory implements LmsAPITemplateFactory { .getBean(MoodleCourseDataAsyncLoader.class); asyncLoaderPrototype.init(lmsSetup.getModelId()); + final MoodleRestTemplateFactory moodleRestTemplateFactory = new MoodleRestTemplateFactory( + this.jsonMapper, + apiTemplateDataSupplier, + this.clientCredentialService, + this.clientHttpRequestFactoryService, + this.alternativeTokenRequestPaths); + if (this.moodlePluginCheck.checkPluginAvailable(lmsSetup)) { - final MoodlePluginCourseAccess moodlePluginCourseAccess = new MoodlePluginCourseAccess(); + final MoodlePluginCourseAccess moodlePluginCourseAccess = new MoodlePluginCourseAccess( + this.jsonMapper, + moodleRestTemplateFactory, + this.cacheManager); final MoodlePluginCourseRestriction moodlePluginCourseRestriction = new MoodlePluginCourseRestriction(); return new LmsAPITemplateAdapter( @@ -101,13 +112,6 @@ public class MoodleLmsAPITemplateFactory implements LmsAPITemplateFactory { } else { - final MoodleRestTemplateFactory moodleRestTemplateFactory = new MoodleRestTemplateFactory( - this.jsonMapper, - apiTemplateDataSupplier, - this.clientCredentialService, - this.clientHttpRequestFactoryService, - this.alternativeTokenRequestPaths); - final MoodleCourseAccess moodleCourseAccess = new MoodleCourseAccess( this.jsonMapper, moodleRestTemplateFactory, diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginCourseAccess.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginCourseAccess.java index 83dc3ba2..001ca9a8 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginCourseAccess.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginCourseAccess.java @@ -8,30 +8,96 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.plugin; +import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Set; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.cache.CacheManager; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import ch.ethz.seb.sebserver.gbl.api.JSONMapper; 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.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.webservice.servicelayer.dao.FilterMap; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.CourseAccessAPI; +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; -public class MoodlePluginCourseAccess implements CourseAccessAPI { +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"; + + 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"; + + private final JSONMapper jsonMapper; + private final MoodleRestTemplateFactory moodleRestTemplateFactory; + + private MoodleAPIRestTemplate restTemplate; + + public MoodlePluginCourseAccess( + final JSONMapper jsonMapper, + final MoodleRestTemplateFactory moodleRestTemplateFactory, + final CacheManager cacheManager) { + super(cacheManager); + this.jsonMapper = jsonMapper; + this.moodleRestTemplateFactory = moodleRestTemplateFactory; + } @Override public LmsSetupTestResult testCourseAccessAPI() { - // TODO Auto-generated method stub - return null; + final LmsSetupTestResult attributesCheck = this.moodleRestTemplateFactory.test(); + if (!attributesCheck.isOk()) { + return attributesCheck; + } + + final Result restTemplateRequest = getRestTemplate(); + if (restTemplateRequest.hasError()) { + final String message = "Failed to gain access token from Moodle Rest API:\n tried token endpoints: " + + this.moodleRestTemplateFactory.knownTokenAccessPaths; + 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()); +// } + + return LmsSetupTestResult.ofOkay(LmsType.MOODLE_PLUGIN); } @Override public Result> getQuizzes(final FilterMap filterMap) { + System.out.println("***************** filterMap: " + filterMap); // TODO Auto-generated method stub - return null; + return Result.of(Collections.emptyList()); } @Override @@ -46,12 +112,6 @@ public class MoodlePluginCourseAccess implements CourseAccessAPI { return null; } - @Override - public void clearCourseCache() { - // TODO Auto-generated method stub - - } - @Override public Result getExamineeAccountDetails(final String examineeUserId) { // TODO Auto-generated method stub @@ -70,4 +130,119 @@ public class MoodlePluginCourseAccess implements CourseAccessAPI { return null; } + @Override + protected Long getLmsSetupId() { + // TODO Auto-generated method stub + return null; + } + + private Result getRestTemplate() { + if (this.restTemplate == null) { + final Result templateRequest = this.moodleRestTemplateFactory + .createRestTemplate(); + if (templateRequest.hasError()) { + return templateRequest; + } else { + this.restTemplate = templateRequest.get(); + } + } + + return Result.of(this.restTemplate); + } + + // ---- Mapping Classes --- + + @JsonIgnoreProperties(ignoreUnknown = true) + private static final class Courses { + final Collection courses; + final Collection warnings; + + @JsonCreator + protected Courses( + @JsonProperty(value = "courses") final Collection courses, + @JsonProperty(value = "warnings") final Collection warnings) { + this.courses = courses; + this.warnings = warnings; + } + } + + /** Maps the Moodle course API course data */ + @JsonIgnoreProperties(ignoreUnknown = true) + private static final class CourseData { + final String id; + final String short_name; + final String idnumber; + final String full_name; + final String display_name; + final Long start_date; // unix-time seconds UTC + final Long end_date; // unix-time seconds UTC + final Long time_created; // unix-time seconds UTC + final Collection quizzes = new ArrayList<>(); + + @JsonCreator + protected CourseData( + @JsonProperty(value = "id") final String id, + @JsonProperty(value = "shortname") final String short_name, + @JsonProperty(value = "idnumber") final String idnumber, + @JsonProperty(value = "fullname") final String full_name, + @JsonProperty(value = "displayname") final String display_name, + @JsonProperty(value = "startdate") final Long start_date, + @JsonProperty(value = "enddate") final Long end_date, + @JsonProperty(value = "timecreated") final Long time_created) { + + this.id = id; + this.short_name = short_name; + this.idnumber = idnumber; + this.full_name = full_name; + this.display_name = display_name; + this.start_date = start_date; + this.end_date = end_date; + this.time_created = time_created; + } + } + + @JsonIgnoreProperties(ignoreUnknown = true) + private static final class CourseQuizData { + final Collection quizzes; + final Collection warnings; + + @JsonCreator + protected CourseQuizData( + @JsonProperty(value = "quizzes") final Collection quizzes, + @JsonProperty(value = "warnings") final Collection warnings) { + this.quizzes = quizzes; + this.warnings = warnings; + } + } + + @JsonIgnoreProperties(ignoreUnknown = true) + static final class CourseQuiz { + final String id; + final String course; + final String course_module; + final String name; + final String intro; // HTML + final Long time_open; // unix-time seconds UTC + final Long time_close; // unix-time seconds UTC + + @JsonCreator + protected CourseQuiz( + @JsonProperty(value = "id") final String id, + @JsonProperty(value = "course") final String course, + @JsonProperty(value = "coursemodule") final String course_module, + @JsonProperty(value = "name") final String name, + @JsonProperty(value = "intro") final String intro, + @JsonProperty(value = "timeopen") final Long time_open, + @JsonProperty(value = "timeclose") final Long time_close) { + + this.id = id; + this.course = course; + this.course_module = course_module; + this.name = name; + this.intro = intro; + this.time_open = time_open; + this.time_close = time_close; + } + } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginCourseRestriction.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginCourseRestriction.java index 162e631f..e6b875d2 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginCourseRestriction.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginCourseRestriction.java @@ -10,6 +10,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.plugin; 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.lms.SEBRestrictionAPI; @@ -19,7 +20,7 @@ public class MoodlePluginCourseRestriction implements SEBRestrictionAPI { @Override public LmsSetupTestResult testCourseRestrictionAPI() { // TODO Auto-generated method stub - return null; + return LmsSetupTestResult.ofOkay(LmsType.MOODLE_PLUGIN); } @Override diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MooldePluginLmsAPITemplateFactory.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MooldePluginLmsAPITemplateFactory.java new file mode 100644 index 00000000..a7016753 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MooldePluginLmsAPITemplateFactory.java @@ -0,0 +1,112 @@ +/* + * 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.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cache.CacheManager; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Lazy; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Service; + +import ch.ethz.seb.sebserver.ClientHttpRequestFactoryService; +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.client.ClientCredentialService; +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.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.MoodleRestTemplateFactory; +import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.legacy.MoodleCourseDataAsyncLoader; + +@Lazy +@Service +@WebServiceProfile +public class MooldePluginLmsAPITemplateFactory implements LmsAPITemplateFactory { + + private final MoodlePluginCheck moodlePluginCheck; + private final JSONMapper jsonMapper; + private final CacheManager cacheManager; + private final AsyncService asyncService; + private final Environment environment; + private final ClientCredentialService clientCredentialService; + private final ClientHttpRequestFactoryService clientHttpRequestFactoryService; + private final ApplicationContext applicationContext; + private final String[] alternativeTokenRequestPaths; + + protected MooldePluginLmsAPITemplateFactory( + final MoodlePluginCheck moodlePluginCheck, + final JSONMapper jsonMapper, + final CacheManager cacheManager, + final AsyncService asyncService, + final Environment environment, + final ClientCredentialService clientCredentialService, + final ClientHttpRequestFactoryService clientHttpRequestFactoryService, + final ApplicationContext applicationContext, + @Value("${sebserver.webservice.lms.moodle.api.token.request.paths:}") final String alternativeTokenRequestPaths) { + + this.moodlePluginCheck = moodlePluginCheck; + this.jsonMapper = jsonMapper; + this.cacheManager = cacheManager; + this.asyncService = asyncService; + this.environment = environment; + this.clientCredentialService = clientCredentialService; + this.clientHttpRequestFactoryService = clientHttpRequestFactoryService; + this.applicationContext = applicationContext; + this.alternativeTokenRequestPaths = (alternativeTokenRequestPaths != null) + ? StringUtils.split(alternativeTokenRequestPaths, Constants.LIST_SEPARATOR) + : null; + } + + @Override + public LmsType lmsType() { + return LmsType.MOODLE_PLUGIN; + } + + @Override + public Result create(final APITemplateDataSupplier apiTemplateDataSupplier) { + return Result.tryCatch(() -> { + + final LmsSetup lmsSetup = apiTemplateDataSupplier.getLmsSetup(); + final MoodleCourseDataAsyncLoader asyncLoaderPrototype = this.applicationContext + .getBean(MoodleCourseDataAsyncLoader.class); + asyncLoaderPrototype.init(lmsSetup.getModelId()); + + final MoodleRestTemplateFactory moodleRestTemplateFactory = new MoodleRestTemplateFactory( + this.jsonMapper, + apiTemplateDataSupplier, + this.clientCredentialService, + this.clientHttpRequestFactoryService, + this.alternativeTokenRequestPaths); + + final MoodlePluginCourseAccess moodlePluginCourseAccess = new MoodlePluginCourseAccess( + this.jsonMapper, + moodleRestTemplateFactory, + this.cacheManager); + + final MoodlePluginCourseRestriction moodlePluginCourseRestriction = new MoodlePluginCourseRestriction(); + + return new LmsAPITemplateAdapter( + this.asyncService, + this.environment, + apiTemplateDataSupplier, + moodlePluginCourseAccess, + moodlePluginCourseRestriction); + + }); + } + +} diff --git a/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/legacy/MoodleCourseAccessTest.java b/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/legacy/MoodleCourseAccessTest.java index 5154e9b0..debbd68d 100644 --- a/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/legacy/MoodleCourseAccessTest.java +++ b/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/legacy/MoodleCourseAccessTest.java @@ -27,7 +27,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.legacy.MoodleRestTemplateFactory.MoodleAPIRestTemplate; +import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory; +import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory.MoodleAPIRestTemplateImpl; public class MoodleCourseAccessTest { @@ -38,7 +39,7 @@ public class MoodleCourseAccessTest { public void testGetExamineeAccountDetails() { final MoodleRestTemplateFactory moodleRestTemplateFactory = mock(MoodleRestTemplateFactory.class); - final MoodleAPIRestTemplate moodleAPIRestTemplate = mock(MoodleAPIRestTemplate.class); + final MoodleAPIRestTemplateImpl moodleAPIRestTemplate = mock(MoodleAPIRestTemplateImpl.class); when(moodleRestTemplateFactory.createRestTemplate()).thenReturn(Result.of(moodleAPIRestTemplate)); when(moodleAPIRestTemplate.callMoodleAPIFunction( anyString(), @@ -132,7 +133,7 @@ public class MoodleCourseAccessTest { @Test public void testInitAPIAccessError2() { final MoodleRestTemplateFactory moodleRestTemplateFactory = mock(MoodleRestTemplateFactory.class); - final MoodleAPIRestTemplate moodleAPIRestTemplate = mock(MoodleAPIRestTemplate.class); + final MoodleAPIRestTemplateImpl moodleAPIRestTemplate = mock(MoodleAPIRestTemplateImpl.class); when(moodleRestTemplateFactory.createRestTemplate()).thenReturn(Result.of(moodleAPIRestTemplate)); doThrow(RuntimeException.class).when(moodleAPIRestTemplate).testAPIConnection(any()); when(moodleRestTemplateFactory.test()).thenReturn(LmsSetupTestResult.ofOkay(LmsType.MOODLE)); @@ -153,7 +154,7 @@ public class MoodleCourseAccessTest { @Test public void testInitAPIAccessOK() { final MoodleRestTemplateFactory moodleRestTemplateFactory = mock(MoodleRestTemplateFactory.class); - final MoodleAPIRestTemplate moodleAPIRestTemplate = mock(MoodleAPIRestTemplate.class); + final MoodleAPIRestTemplateImpl moodleAPIRestTemplate = mock(MoodleAPIRestTemplateImpl.class); when(moodleRestTemplateFactory.createRestTemplate()).thenReturn(Result.of(moodleAPIRestTemplate)); when(moodleRestTemplateFactory.test()).thenReturn(LmsSetupTestResult.ofOkay(LmsType.MOODLE));