SEBSERV-301 implementation

This commit is contained in:
anhefti 2022-12-08 16:36:24 +01:00
parent a141eccfa1
commit 71ae9fc755
12 changed files with 471 additions and 108 deletions

View file

@ -270,4 +270,14 @@ public class POSTMapper {
this.params.putIfAbsent(name, Arrays.asList(value)); this.params.putIfAbsent(name, Arrays.asList(value));
return (T) this; 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();
}
} }

View file

@ -60,6 +60,8 @@ public final class LmsSetup implements GrantEntity, Activatable {
OPEN_EDX(Features.COURSE_API, Features.SEB_RESTRICTION), OPEN_EDX(Features.COURSE_API, Features.SEB_RESTRICTION),
/** The Moodle binding features only the course access API so far */ /** The Moodle binding features only the course access API so far */
MOODLE(Features.COURSE_API /* , Features.SEB_RESTRICTION */), 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 */ /** The Ans Delft binding is on the way */
ANS_DELFT(Features.COURSE_API, Features.SEB_RESTRICTION), ANS_DELFT(Features.COURSE_API, Features.SEB_RESTRICTION),
/** The OpenOLAT binding is on the way */ /** The OpenOLAT binding is on the way */

View file

@ -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<String, String> queryAttributes);
String callMoodleAPIFunction(
final String functionName,
final MultiValueMap<String, String> queryParams,
final MultiValueMap<String, String> 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();
}
}
}

View file

@ -6,7 +6,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/ */
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.legacy; package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -57,11 +57,11 @@ public class MoodleRestTemplateFactory {
private static final Logger log = LoggerFactory.getLogger(MoodleRestTemplateFactory.class); private static final Logger log = LoggerFactory.getLogger(MoodleRestTemplateFactory.class);
final JSONMapper jsonMapper; public final JSONMapper jsonMapper;
final APITemplateDataSupplier apiTemplateDataSupplier; public final APITemplateDataSupplier apiTemplateDataSupplier;
final ClientHttpRequestFactoryService clientHttpRequestFactoryService; public final ClientHttpRequestFactoryService clientHttpRequestFactoryService;
final ClientCredentialService clientCredentialService; public final ClientCredentialService clientCredentialService;
final Set<String> knownTokenAccessPaths; public final Set<String> knownTokenAccessPaths;
public MoodleRestTemplateFactory( public MoodleRestTemplateFactory(
final JSONMapper jsonMapper, final JSONMapper jsonMapper,
@ -75,11 +75,12 @@ public class MoodleRestTemplateFactory {
this.clientCredentialService = clientCredentialService; this.clientCredentialService = clientCredentialService;
this.clientHttpRequestFactoryService = clientHttpRequestFactoryService; this.clientHttpRequestFactoryService = clientHttpRequestFactoryService;
this.knownTokenAccessPaths = new HashSet<>(); final Set<String> paths = new HashSet<>();
this.knownTokenAccessPaths.add(MoodleAPIRestTemplate.MOODLE_DEFAULT_TOKEN_REQUEST_PATH); paths.add(MoodleAPIRestTemplate.MOODLE_DEFAULT_TOKEN_REQUEST_PATH);
if (alternativeTokenRequestPaths != null) { if (alternativeTokenRequestPaths != null) {
this.knownTokenAccessPaths.addAll(Arrays.asList(alternativeTokenRequestPaths)); paths.addAll(Arrays.asList(alternativeTokenRequestPaths));
} }
this.knownTokenAccessPaths = Utils.immutableSetOf(paths);
} }
APITemplateDataSupplier getApiTemplateDataSupplier() { APITemplateDataSupplier getApiTemplateDataSupplier() {
@ -125,7 +126,7 @@ public class MoodleRestTemplateFactory {
return LmsSetupTestResult.ofOkay(LmsType.MOODLE); return LmsSetupTestResult.ofOkay(LmsType.MOODLE);
} }
Result<MoodleAPIRestTemplate> createRestTemplate() { public Result<MoodleAPIRestTemplate> createRestTemplate() {
final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup(); final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup();
@ -149,7 +150,7 @@ public class MoodleRestTemplateFactory {
") on paths: " + this.knownTokenAccessPaths)); ") on paths: " + this.knownTokenAccessPaths));
} }
Result<MoodleAPIRestTemplate> createRestTemplate(final String accessTokenPath) { public Result<MoodleAPIRestTemplate> createRestTemplate(final String accessTokenPath) {
final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup(); final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup();
@ -162,8 +163,9 @@ public class MoodleRestTemplateFactory {
.getPlainClientSecret(credentials) .getPlainClientSecret(credentials)
.getOrThrow(); .getOrThrow();
final MoodleAPIRestTemplate restTemplate = new MoodleAPIRestTemplate( final MoodleAPIRestTemplateImpl restTemplate = new MoodleAPIRestTemplateImpl(
this.jsonMapper, this.jsonMapper,
this.apiTemplateDataSupplier,
lmsSetup.lmsApiUrl, lmsSetup.lmsApiUrl,
accessTokenPath, accessTokenPath,
lmsSetup.lmsRestApiToken, 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"; 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 serverURL;
private final String tokenPath; private final String tokenPath;
@ -211,14 +204,18 @@ public class MoodleRestTemplateFactory {
private final Map<String, String> tokenReqURIVars; private final Map<String, String> tokenReqURIVars;
private final HttpEntity<?> tokenReqEntity = new HttpEntity<>(new LinkedMultiValueMap<>()); private final HttpEntity<?> tokenReqEntity = new HttpEntity<>(new LinkedMultiValueMap<>());
protected MoodleAPIRestTemplate( protected MoodleAPIRestTemplateImpl(
final JSONMapper jsonMapper, final JSONMapper jsonMapper,
final APITemplateDataSupplier apiTemplateDataSupplier,
final String serverURL, final String serverURL,
final String tokenPath, final String tokenPath,
final CharSequence accessToken, final CharSequence accessToken,
final CharSequence username, final CharSequence username,
final CharSequence password) { final CharSequence password) {
this.jsonMapper = jsonMapper;
this.apiTemplateDataSupplier = apiTemplateDataSupplier;
this.serverURL = serverURL; this.serverURL = serverURL;
this.tokenPath = tokenPath; this.tokenPath = tokenPath;
this.accessToken = StringUtils.isNotBlank(accessToken) ? accessToken : null; this.accessToken = StringUtils.isNotBlank(accessToken) ? accessToken : null;
@ -230,14 +227,17 @@ public class MoodleRestTemplateFactory {
} }
@Override
public String getService() { public String getService() {
return this.tokenReqURIVars.get(URI_VAR_SERVICE); return this.tokenReqURIVars.get(URI_VAR_SERVICE);
} }
@Override
public void setService(final String service) { public void setService(final String service) {
this.tokenReqURIVars.put(URI_VAR_SERVICE, service); this.tokenReqURIVars.put(URI_VAR_SERVICE, service);
} }
@Override
public CharSequence getAccessToken() { public CharSequence getAccessToken() {
if (this.accessToken == null) { if (this.accessToken == null) {
requestAccessToken(); requestAccessToken();
@ -246,22 +246,27 @@ public class MoodleRestTemplateFactory {
return this.accessToken; return this.accessToken;
} }
@Override
public void testAPIConnection(final String... functions) { public void testAPIConnection(final String... functions) {
try { try {
final String apiInfo = this.callMoodleAPIFunction(REST_API_TEST_FUNCTION); final String apiInfo = this.callMoodleAPIFunction(REST_API_TEST_FUNCTION);
final WebserviceInfo webserviceInfo = final WebserviceInfo webserviceInfo = this.jsonMapper.readValue(
MoodleRestTemplateFactory.this.jsonMapper.readValue(apiInfo, WebserviceInfo.class); apiInfo,
WebserviceInfo.class);
if (StringUtils.isBlank(webserviceInfo.username) || StringUtils.isBlank(webserviceInfo.userid)) { if (StringUtils.isBlank(webserviceInfo.username) || StringUtils.isBlank(webserviceInfo.userid)) {
throw new RuntimeException("Invalid WebserviceInfo: " + webserviceInfo); throw new RuntimeException("Invalid WebserviceInfo: " + webserviceInfo);
} }
final List<String> missingAPIFunctions = Arrays.stream(functions) if (functions != null) {
.filter(f -> !webserviceInfo.functions.containsKey(f))
.collect(Collectors.toList());
if (!missingAPIFunctions.isEmpty()) { final List<String> missingAPIFunctions = Arrays.stream(functions)
throw new RuntimeException("Missing Moodle Webservice API functions: " + missingAPIFunctions); .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) { } catch (final RuntimeException re) {
@ -271,16 +276,19 @@ public class MoodleRestTemplateFactory {
} }
} }
@Override
public String callMoodleAPIFunction(final String functionName) { public String callMoodleAPIFunction(final String functionName) {
return callMoodleAPIFunction(functionName, null, null); return callMoodleAPIFunction(functionName, null, null);
} }
@Override
public String callMoodleAPIFunction( public String callMoodleAPIFunction(
final String functionName, final String functionName,
final MultiValueMap<String, String> queryAttributes) { final MultiValueMap<String, String> queryAttributes) {
return callMoodleAPIFunction(functionName, null, queryAttributes); return callMoodleAPIFunction(functionName, null, queryAttributes);
} }
@Override
public String callMoodleAPIFunction( public String callMoodleAPIFunction(
final String functionName, final String functionName,
final MultiValueMap<String, String> queryParams, final MultiValueMap<String, String> queryParams,
@ -319,9 +327,7 @@ public class MoodleRestTemplateFactory {
functionReqEntity, functionReqEntity,
String.class); String.class);
final LmsSetup lmsSetup = MoodleRestTemplateFactory.this.apiTemplateDataSupplier final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup();
.getLmsSetup();
if (response.getStatusCode() != HttpStatus.OK) { if (response.getStatusCode() != HttpStatus.OK) {
throw new RuntimeException( throw new RuntimeException(
"Failed to call Moodle webservice API function: " + functionName + " lms setup: " + "Failed to call Moodle webservice API function: " + functionName + " lms setup: " +
@ -347,9 +353,7 @@ public class MoodleRestTemplateFactory {
private void requestAccessToken() { private void requestAccessToken() {
final LmsSetup lmsSetup = MoodleRestTemplateFactory.this.apiTemplateDataSupplier final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup();
.getLmsSetup();
try { try {
final ResponseEntity<String> response = super.exchange( final ResponseEntity<String> response = super.exchange(
@ -369,7 +373,7 @@ public class MoodleRestTemplateFactory {
} }
try { try {
final MoodleToken moodleToken = MoodleRestTemplateFactory.this.jsonMapper.readValue( final MoodleToken moodleToken = this.jsonMapper.readValue(
response.getBody(), response.getBody(),
MoodleToken.class); MoodleToken.class);

View file

@ -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.APITemplateDataSupplier;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.CourseAccessAPI; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.CourseAccessAPI;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleAPIRestTemplate;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleAPIRestTemplate.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.CourseDataShort;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.legacy.MoodleCourseDataAsyncLoader.CourseQuizShort; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.legacy.MoodleCourseDataAsyncLoader.CourseQuizShort;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.legacy.MoodleRestTemplateFactory.MoodleAPIRestTemplate;
/** Implements the LmsAPITemplate for Open edX LMS Course API access. /** 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();
}
}
} }

View file

@ -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.async.CircuitBreaker;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Utils; import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.legacy.MoodleCourseAccess.Warning; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleAPIRestTemplate;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.legacy.MoodleRestTemplateFactory.MoodleAPIRestTemplate; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleAPIRestTemplate.Warning;
@Lazy @Lazy
@Component @Component

View file

@ -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.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionAPI; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionAPI;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.NoSEBRestrictionException; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.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: /** GET:
* http://yourmoodle.org/webservice/rest/server.php?wstoken={token}&moodlewsrestformat=json&wsfunction=seb_restriction&courseId=123 * http://yourmoodle.org/webservice/rest/server.php?wstoken={token}&moodlewsrestformat=json&wsfunction=seb_restriction&courseId=123

View file

@ -6,10 +6,11 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/ */
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle; package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.legacy;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.CacheManager;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.core.env.Environment; 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.LmsAPITemplate;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplateFactory; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplateFactory;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.LmsAPITemplateAdapter; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.LmsAPITemplateAdapter;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.legacy.MoodleCourseAccess; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory;
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.plugin.MoodlePluginCheck; 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.MoodlePluginCourseAccess;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.plugin.MoodlePluginCourseRestriction; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.plugin.MoodlePluginCourseRestriction;
@ -43,6 +41,7 @@ public class MoodleLmsAPITemplateFactory implements LmsAPITemplateFactory {
private final MoodlePluginCheck moodlePluginCheck; private final MoodlePluginCheck moodlePluginCheck;
private final JSONMapper jsonMapper; private final JSONMapper jsonMapper;
private final CacheManager cacheManager;
private final AsyncService asyncService; private final AsyncService asyncService;
private final Environment environment; private final Environment environment;
private final ClientCredentialService clientCredentialService; private final ClientCredentialService clientCredentialService;
@ -53,6 +52,7 @@ public class MoodleLmsAPITemplateFactory implements LmsAPITemplateFactory {
protected MoodleLmsAPITemplateFactory( protected MoodleLmsAPITemplateFactory(
final MoodlePluginCheck moodlePluginCheck, final MoodlePluginCheck moodlePluginCheck,
final JSONMapper jsonMapper, final JSONMapper jsonMapper,
final CacheManager cacheManager,
final AsyncService asyncService, final AsyncService asyncService,
final Environment environment, final Environment environment,
final ClientCredentialService clientCredentialService, final ClientCredentialService clientCredentialService,
@ -62,6 +62,7 @@ public class MoodleLmsAPITemplateFactory implements LmsAPITemplateFactory {
this.moodlePluginCheck = moodlePluginCheck; this.moodlePluginCheck = moodlePluginCheck;
this.jsonMapper = jsonMapper; this.jsonMapper = jsonMapper;
this.cacheManager = cacheManager;
this.asyncService = asyncService; this.asyncService = asyncService;
this.environment = environment; this.environment = environment;
this.clientCredentialService = clientCredentialService; this.clientCredentialService = clientCredentialService;
@ -87,9 +88,19 @@ public class MoodleLmsAPITemplateFactory implements LmsAPITemplateFactory {
.getBean(MoodleCourseDataAsyncLoader.class); .getBean(MoodleCourseDataAsyncLoader.class);
asyncLoaderPrototype.init(lmsSetup.getModelId()); asyncLoaderPrototype.init(lmsSetup.getModelId());
final MoodleRestTemplateFactory moodleRestTemplateFactory = new MoodleRestTemplateFactory(
this.jsonMapper,
apiTemplateDataSupplier,
this.clientCredentialService,
this.clientHttpRequestFactoryService,
this.alternativeTokenRequestPaths);
if (this.moodlePluginCheck.checkPluginAvailable(lmsSetup)) { 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(); final MoodlePluginCourseRestriction moodlePluginCourseRestriction = new MoodlePluginCourseRestriction();
return new LmsAPITemplateAdapter( return new LmsAPITemplateAdapter(
@ -101,13 +112,6 @@ public class MoodleLmsAPITemplateFactory implements LmsAPITemplateFactory {
} else { } else {
final MoodleRestTemplateFactory moodleRestTemplateFactory = new MoodleRestTemplateFactory(
this.jsonMapper,
apiTemplateDataSupplier,
this.clientCredentialService,
this.clientHttpRequestFactoryService,
this.alternativeTokenRequestPaths);
final MoodleCourseAccess moodleCourseAccess = new MoodleCourseAccess( final MoodleCourseAccess moodleCourseAccess = new MoodleCourseAccess(
this.jsonMapper, this.jsonMapper,
moodleRestTemplateFactory, moodleRestTemplateFactory,

View file

@ -8,30 +8,96 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.plugin; package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.plugin;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Set; 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.Chapters;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData; import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails; import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.CourseAccessAPI; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.CourseAccessAPI;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.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 @Override
public LmsSetupTestResult testCourseAccessAPI() { public LmsSetupTestResult testCourseAccessAPI() {
// TODO Auto-generated method stub final LmsSetupTestResult attributesCheck = this.moodleRestTemplateFactory.test();
return null; 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.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 @Override
public Result<List<QuizData>> getQuizzes(final FilterMap filterMap) { public Result<List<QuizData>> getQuizzes(final FilterMap filterMap) {
System.out.println("***************** filterMap: " + filterMap);
// TODO Auto-generated method stub // TODO Auto-generated method stub
return null; return Result.of(Collections.emptyList());
} }
@Override @Override
@ -46,12 +112,6 @@ public class MoodlePluginCourseAccess implements CourseAccessAPI {
return null; return null;
} }
@Override
public void clearCourseCache() {
// TODO Auto-generated method stub
}
@Override @Override
public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeUserId) { public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeUserId) {
// TODO Auto-generated method stub // TODO Auto-generated method stub
@ -70,4 +130,119 @@ public class MoodlePluginCourseAccess implements CourseAccessAPI {
return null; return null;
} }
@Override
protected Long getLmsSetupId() {
// TODO Auto-generated method stub
return null;
}
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);
}
// ---- 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

@ -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.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction; import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionAPI; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionAPI;
@ -19,7 +20,7 @@ public class MoodlePluginCourseRestriction implements SEBRestrictionAPI {
@Override @Override
public LmsSetupTestResult testCourseRestrictionAPI() { public LmsSetupTestResult testCourseRestrictionAPI() {
// TODO Auto-generated method stub // TODO Auto-generated method stub
return null; return LmsSetupTestResult.ofOkay(LmsType.MOODLE_PLUGIN);
} }
@Override @Override

View file

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

View file

@ -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.institution.LmsSetupTestResult.ErrorType;
import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails; import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.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 { public class MoodleCourseAccessTest {
@ -38,7 +39,7 @@ public class MoodleCourseAccessTest {
public void testGetExamineeAccountDetails() { public void testGetExamineeAccountDetails() {
final MoodleRestTemplateFactory moodleRestTemplateFactory = mock(MoodleRestTemplateFactory.class); 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.createRestTemplate()).thenReturn(Result.of(moodleAPIRestTemplate));
when(moodleAPIRestTemplate.callMoodleAPIFunction( when(moodleAPIRestTemplate.callMoodleAPIFunction(
anyString(), anyString(),
@ -132,7 +133,7 @@ public class MoodleCourseAccessTest {
@Test @Test
public void testInitAPIAccessError2() { public void testInitAPIAccessError2() {
final MoodleRestTemplateFactory moodleRestTemplateFactory = mock(MoodleRestTemplateFactory.class); 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.createRestTemplate()).thenReturn(Result.of(moodleAPIRestTemplate));
doThrow(RuntimeException.class).when(moodleAPIRestTemplate).testAPIConnection(any()); doThrow(RuntimeException.class).when(moodleAPIRestTemplate).testAPIConnection(any());
when(moodleRestTemplateFactory.test()).thenReturn(LmsSetupTestResult.ofOkay(LmsType.MOODLE)); when(moodleRestTemplateFactory.test()).thenReturn(LmsSetupTestResult.ofOkay(LmsType.MOODLE));
@ -153,7 +154,7 @@ public class MoodleCourseAccessTest {
@Test @Test
public void testInitAPIAccessOK() { public void testInitAPIAccessOK() {
final MoodleRestTemplateFactory moodleRestTemplateFactory = mock(MoodleRestTemplateFactory.class); 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.createRestTemplate()).thenReturn(Result.of(moodleAPIRestTemplate));
when(moodleRestTemplateFactory.test()).thenReturn(LmsSetupTestResult.ofOkay(LmsType.MOODLE)); when(moodleRestTemplateFactory.test()).thenReturn(LmsSetupTestResult.ofOkay(LmsType.MOODLE));