simplified LMS API

This commit is contained in:
anhefti 2021-05-17 19:26:26 +02:00
parent 0d8fb4b880
commit 0b00995aa7
13 changed files with 117 additions and 230 deletions

View file

@ -37,8 +37,6 @@ public interface ExamDAO extends ActivatableEntityDAO<Exam, Exam>, BulkActionSup
* happened */
Result<GrantEntity> examGrantEntityByClientConnection(Long connectionId);
Result<Exam> getWithQuizDataFromCache(Long id);
/** Get all active Exams for a given institution.
*
* @param institutionId the identifier of the institution

View file

@ -392,7 +392,7 @@ public class ExamConfigurationMapDAOImpl implements ExamConfigurationMapDAO {
final String status = config.getStatus();
final Exam exam = this.examDAO
.getWithQuizDataFromCache(record.getExamId())
.byPK(record.getExamId())
.getOr(null);
return new ExamConfigurationMap(

View file

@ -119,13 +119,6 @@ public class ExamDAOImpl implements ExamDAO {
.map(record -> toDomainModel(record, null, null).getOrThrow());
}
@Override
@Transactional(readOnly = true)
public Result<Exam> getWithQuizDataFromCache(final Long id) {
return recordById(id)
.flatMap(this::toDomainModelFromCache);
}
@Override
@Transactional(readOnly = true)
public Result<Collection<Exam>> all(final Long institutionId, final Boolean active) {
@ -169,7 +162,6 @@ public class ExamDAOImpl implements ExamDAO {
return Result.tryCatch(() -> {
final boolean cached = filterMap.getBoolean(Exam.FILTER_CACHED_QUIZZES);
final String name = filterMap.getQuizName();
final DateTime from = filterMap.getExamFromTime();
final Predicate<Exam> quizDataFilter = exam -> {
@ -236,7 +228,7 @@ public class ExamDAOImpl implements ExamDAO {
.build()
.execute();
return this.toDomainModel(records, cached)
return this.toDomainModel(records)
.getOrThrow()
.stream()
.filter(quizDataFilter.and(predicate))
@ -768,17 +760,6 @@ public class ExamDAOImpl implements ExamDAO {
exam.getDescription());
}
private Result<Exam> toDomainModelFromCache(final ExamRecord record) {
return this.lmsAPIService
.getLmsAPITemplate(record.getLmsSetupId())
.flatMap(template -> this.toDomainModel(
record,
template.getQuizFromCache(record.getExternalId())
.getOrThrow(),
null));
}
private Result<Exam> toDomainModel(final ExamRecord record) {
return toDomainModel(
record.getLmsSetupId(),
@ -787,12 +768,6 @@ public class ExamDAOImpl implements ExamDAO {
}
private Result<Collection<Exam>> toDomainModel(final Collection<ExamRecord> records) {
return toDomainModel(records, false);
}
private Result<Collection<Exam>> toDomainModel(
final Collection<ExamRecord> records,
final boolean cached) {
return Result.tryCatch(() -> {
@ -807,8 +782,7 @@ public class ExamDAOImpl implements ExamDAO {
.stream()
.flatMap(entry -> toDomainModel(
entry.getKey(),
entry.getValue(),
cached)
entry.getValue())
.onError(error -> log.error(
"Failed to get quizzes from LMS Setup: {}",
entry.getKey(), error))
@ -822,14 +796,6 @@ public class ExamDAOImpl implements ExamDAO {
final Long lmsSetupId,
final Collection<ExamRecord> records) {
return toDomainModel(lmsSetupId, records, false);
}
private Result<Collection<Exam>> toDomainModel(
final Long lmsSetupId,
final Collection<ExamRecord> records,
final boolean cached) {
return Result.tryCatch(() -> {
// map records
@ -840,7 +806,7 @@ public class ExamDAOImpl implements ExamDAO {
// get and map quizzes
final Map<String, QuizData> quizzes = this.lmsAPIService
.getLmsAPITemplate(lmsSetupId)
.map(template -> getQuizzesFromLMS(template, recordMapping.keySet(), cached))
.map(template -> getQuizzesFromLMS(template, recordMapping.keySet()))
.onError(error -> log.error("Failed to get quizzes for exams: ", error))
.getOr(Collections.emptyList())
.stream()
@ -894,13 +860,10 @@ public class ExamDAOImpl implements ExamDAO {
private Collection<Result<QuizData>> getQuizzesFromLMS(
final LmsAPITemplate template,
final Set<String> ids,
final boolean cached) {
final Set<String> ids) {
try {
return (cached)
? template.getQuizzesFromCache(ids)
: template.getQuizzes(ids);
return template.getQuizzes(ids);
} catch (final Exception e) {
log.error("Unexpected error while using LmsAPITemplate to get quizzes: ", e);
return Collections.emptyList();

View file

@ -8,15 +8,10 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.lms;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.async.MemoizingCircuitBreaker;
import ch.ethz.seb.sebserver.gbl.model.exam.Chapters;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
@ -27,7 +22,6 @@ 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.dao.ResourceNotFoundException;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.AbstractCourseAccess;
/** Defines an LMS API access template to build SEB Server LMS integration.
@ -135,41 +129,10 @@ public interface LmsAPITemplate {
/** Get the quiz data with specified identifier.
*
* Default implementation: Uses {@link #getQuizzes(Set<String> ids) } and returns the first matching or an error.
*
* @param id the quiz data identifier
* @return Result refer to the quiz data or to an error when happened */
default Result<QuizData> getQuiz(final String id) {
if (StringUtils.isBlank(id)) {
return Result.ofError(new RuntimeException("missing model id"));
}
return getQuizzes(new HashSet<>(Arrays.asList(id)))
.stream()
.findFirst()
.orElse(Result.ofError(new ResourceNotFoundException(EntityType.EXAM, id)));
}
/** Get all {@link QuizData } for the set of {@link QuizData }-identifiers (ids) from the LMS defined within the
* underling LmsSetup, in a collection of Results.
*
* If there is caching involved this function shall try to get the data from the cache first.
*
* NOTE: This function depends on the specific LMS implementation and on whether caching the quiz data
* makes sense or not. Following strategy is recommended:
* Looks first in the cache if the whole set of {@link QuizData } can be get from the cache.
* If all quizzes are cached, returns all from cache.
* If one or more quiz is not in the cache, requests all quizzes from the API and refreshes the cache
*
* @param ids the Set of Quiz identifiers to get the {@link QuizData } for
* @return Collection of all {@link QuizData } from the given id set */
Collection<Result<QuizData>> getQuizzesFromCache(Set<String> ids);
/** Get a particular quiz data from cache if available. If not, tries to get it from the LMS.
*
* @param id the quiz identifier, external identifier of the exam.
* @return Result refer to the {@link QuizData } or to an error when happended */
Result<QuizData> getQuizFromCache(String id);
Result<QuizData> getQuiz(final String id);
/** Clears the underling caches if there are some for a particular implementation. */
void clearCache();

View file

@ -9,8 +9,6 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl;
import java.util.Collection;
import java.util.Set;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -21,7 +19,6 @@ import org.springframework.core.env.Environment;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.async.AsyncService;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.util.Result;
/** This implements an overall short time cache for QuizData objects for all implementing
* instances. It uses EH-Cache with a short time to live about 1 - 2 minutes.
@ -97,11 +94,6 @@ public abstract class AbstractCachedCourseAccess extends AbstractCourseAccess {
this.cache.evict(createCacheKey);
}
@Override
public Result<Collection<Result<QuizData>>> getQuizzesFromCache(final Set<String> ids) {
return Result.of(ids.stream().map(this::getQuizFromCache).collect(Collectors.toList()));
}
/** Get the LMS setup identifier that is wrapped within the implementing template.
* This is used to create the cache Key.
*

View file

@ -8,7 +8,6 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
@ -135,28 +134,6 @@ public abstract class AbstractCourseAccess {
Collections.emptyMap());
}
/** This abstraction has no cache implementation and therefore this returns a Result
* with an "No cache supported error.
* </p>
* To implement and use caching, this must be overridden and implemented
*
* @param id The identifier of the QuizData to get from cache
* @return Result with an "No cache supported error */
public Result<QuizData> getQuizFromCache(final String id) {
return Result.ofRuntimeError("No cache supported");
}
/** This abstraction has no cache implementation and therefore this returns a Result
* with an "No cache supported error.
* </p>
* To implement and use caching, this must be overridden and implemented
*
* @param ids Collection of quiz data identifier to get from the cache
* @return Result with an "No cache supported error */
public Result<Collection<Result<QuizData>>> getQuizzesFromCache(final Set<String> ids) {
return Result.ofRuntimeError("No cache supported");
}
/** Provides a supplier for the quiz data request to use within the circuit breaker */
protected abstract Supplier<List<QuizData>> quizzesSupplier(final Set<String> ids);

View file

@ -12,9 +12,12 @@ import java.net.URL;
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.NoSuchElementException;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@ -195,29 +198,52 @@ final class OpenEdxCourseAccess extends AbstractCachedCourseAccess {
.getOrThrow();
}
@Override
public Result<QuizData> getQuizFromCache(final String id) {
return Result.tryCatch(() -> {
public Collection<Result<QuizData>> getQuizzesFromCache(final Set<String> ids) {
final HashSet<String> leftIds = new HashSet<>(ids);
final Collection<Result<QuizData>> result = new ArrayList<>();
ids.stream()
.map(this::getQuizFromCache)
.forEach(q -> {
if (q != null) {
leftIds.remove(q.id);
result.add(Result.of(q));
}
});
// first try to get it from short time cache
QuizData quizData = super.getFromCache(id);
if (quizData != null) {
return quizData;
}
if (!leftIds.isEmpty()) {
super.quizzesRequest.protectedRun(this.quizzesSupplier(leftIds))
.onError(error -> log.error("Failed to get quizzes by ids: ", error))
.getOrElse(() -> Collections.emptyList())
.stream()
.forEach(q -> {
leftIds.remove(q.id);
result.add(Result.of(q));
});
}
// Otherwise get one course from LMS and cache
final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup();
final String externalStartURI = getExternalLMSServerAddress(lmsSetup);
quizData = quizDataOf(
lmsSetup,
this.getOneCourse(id, this.restTemplate, id),
externalStartURI);
if (!leftIds.isEmpty()) {
leftIds.forEach(q -> result.add(Result.ofError(new NoSuchElementException())));
}
if (quizData != null) {
super.putToCache(quizData);
}
return quizData;
});
return result;
}
public QuizData getQuizFromCache(final String id) {
return super.getFromCache(id);
}
public QuizData getQuizFromLMS(final String id) {
final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup();
final String externalStartURI = getExternalLMSServerAddress(lmsSetup);
final QuizData quizData = quizDataOf(
lmsSetup,
this.getOneCourse(id, this.restTemplate, id),
externalStartURI);
if (quizData != null) {
super.putToCache(quizData);
}
return quizData;
}
@Override

View file

@ -10,9 +10,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.edx;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.slf4j.Logger;
@ -82,33 +80,21 @@ final class OpenEdxLmsAPITemplate implements LmsAPITemplate {
.collect(Collectors.toList()));
}
@Override
public Result<QuizData> getQuiz(final String id) {
return Result.tryCatch(() -> {
final QuizData quizFromCache = this.openEdxCourseAccess.getQuizFromCache(id);
if (quizFromCache != null) {
return quizFromCache;
}
return this.openEdxCourseAccess.getQuizFromLMS(id);
});
}
@Override
public Collection<Result<QuizData>> getQuizzes(final Set<String> ids) {
final Map<String, QuizData> mapping = this.openEdxCourseAccess
.quizzesSupplier(ids)
.get()
.stream()
.collect(Collectors.toMap(qd -> qd.id, Function.identity()));
return ids.stream()
.map(id -> {
final QuizData data = mapping.get(id);
return (data == null) ? Result.<QuizData> ofRuntimeError("Missing id: " + id) : Result.of(data);
})
.collect(Collectors.toList());
}
@Override
public Result<QuizData> getQuizFromCache(final String id) {
return this.openEdxCourseAccess
.getQuizFromCache(id)
.orElse(() -> getQuiz(id));
}
@Override
public Collection<Result<QuizData>> getQuizzesFromCache(final Set<String> ids) {
return this.openEdxCourseAccess.getQuizzesFromCache(ids)
.getOrElse(() -> getQuizzes(ids));
return this.openEdxCourseAccess.getQuizzesFromCache(ids);
}
@Override

View file

@ -9,9 +9,7 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.mockup;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@ -164,6 +162,15 @@ public class MockupLmsAPITemplate implements LmsAPITemplate {
});
}
@Override
public Result<QuizData> getQuiz(final String id) {
return Result.of(this.mockups
.stream()
.filter(q -> id.equals(q.id))
.findFirst()
.get());
}
@Override
public Collection<Result<QuizData>> getQuizzes(final Set<String> ids) {
if (!authenticate()) {
@ -178,16 +185,6 @@ public class MockupLmsAPITemplate implements LmsAPITemplate {
.collect(Collectors.toList());
}
@Override
public Collection<Result<QuizData>> getQuizzesFromCache(final Set<String> ids) {
return getQuizzes(ids);
}
@Override
public Result<QuizData> getQuizFromCache(final String id) {
return getQuizzes(new HashSet<>(Arrays.asList(id))).iterator().next();
}
@Override
public void clearCache() {

View file

@ -20,6 +20,7 @@ import java.util.function.Function;
import java.util.function.Supplier;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
@ -213,7 +214,6 @@ public class MoodleCourseAccess extends AbstractCourseAccess {
return LmsSetupTestResult.ofOkay(LmsType.MOODLE);
}
@Override
public Result<QuizData> getQuizFromCache(final String id) {
return Result.tryCatch(() -> {
@ -245,43 +245,48 @@ public class MoodleCourseAccess extends AbstractCourseAccess {
}
}
throw new RuntimeException("No quiz found in cache");
// get from LMS
final Set<String> ids = Stream.of(id).collect(Collectors.toSet());
return super.quizzesRequest
.protectedRun(quizzesSupplier(ids))
.getOrThrow()
.get(0);
});
}
@Override
public Result<Collection<Result<QuizData>>> getQuizzesFromCache(final Set<String> ids) {
return Result.tryCatch(() -> {
final List<QuizData> cached = getCached();
final List<QuizData> available = (cached != null)
? cached
: Collections.emptyList();
public Collection<Result<QuizData>> getQuizzesFromCache(final Set<String> ids) {
final List<QuizData> cached = getCached();
final List<QuizData> available = (cached != null)
? cached
: Collections.emptyList();
final Map<String, QuizData> quizMapping = available
final Map<String, QuizData> quizMapping = available
.stream()
.collect(Collectors.toMap(q -> q.id, Function.identity()));
if (!quizMapping.keySet().containsAll(ids)) {
final Map<String, QuizData> collect = super.quizzesRequest
.protectedRun(quizzesSupplier(ids))
.onError(error -> log.error("Failed to get quizzes by ids: ", error))
.getOrElse(() -> Collections.emptyList())
.stream()
.collect(Collectors.toMap(q -> q.id, Function.identity()));
if (!quizMapping.keySet().containsAll(ids)) {
final Map<String, QuizData> collect = quizzesSupplier(ids).get()
.stream()
.collect(Collectors.toMap(qd -> qd.id, Function.identity()));
if (collect != null) {
quizMapping.clear();
quizMapping.putAll(collect);
}
.collect(Collectors.toMap(qd -> qd.id, Function.identity()));
if (collect != null) {
quizMapping.clear();
quizMapping.putAll(collect);
}
}
return ids
.stream()
.map(id -> {
final QuizData q = quizMapping.get(id);
return (q == null)
? Result.<QuizData> ofError(new NoSuchElementException("Quiz with id: " + id))
: Result.of(q);
})
.collect(Collectors.toList());
});
return ids
.stream()
.map(id -> {
final QuizData q = quizMapping.get(id);
return (q == null)
? Result.<QuizData> ofError(new NoSuchElementException("Quiz with id: " + id))
: Result.of(q);
})
.collect(Collectors.toList());
}
@Override

View file

@ -10,9 +10,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.slf4j.Logger;
@ -95,25 +93,13 @@ public class MoodleLmsAPITemplate implements LmsAPITemplate {
}
@Override
public Collection<Result<QuizData>> getQuizzes(final Set<String> ids) {
final Map<String, QuizData> mapping = this.moodleCourseAccess
.quizzesSupplier(ids)
.get()
.stream()
.collect(Collectors.toMap(qd -> qd.id, Function.identity()));
return ids.stream()
.map(id -> {
final QuizData data = mapping.get(id);
return (data == null) ? Result.<QuizData> ofRuntimeError("Missing id: " + id) : Result.of(data);
})
.collect(Collectors.toList());
public Result<QuizData> getQuiz(final String id) {
return this.moodleCourseAccess.getQuizFromCache(id);
}
@Override
public Result<QuizData> getQuizFromCache(final String id) {
return this.moodleCourseAccess.getQuizFromCache(id)
.orElse(() -> getQuiz(id));
public Collection<Result<QuizData>> getQuizzes(final Set<String> ids) {
return this.moodleCourseAccess.getQuizzesFromCache(ids);
}
@Override
@ -121,12 +107,6 @@ public class MoodleLmsAPITemplate implements LmsAPITemplate {
this.moodleCourseAccess.clearCache();
}
@Override
public Collection<Result<QuizData>> getQuizzesFromCache(final Set<String> ids) {
return this.moodleCourseAccess.getQuizzesFromCache(ids)
.getOrElse(() -> getQuizzes(ids));
}
@Override
public Result<Chapters> getCourseChapters(final String courseId) {
return Result.tryCatch(() -> this.moodleCourseAccess

View file

@ -124,7 +124,7 @@ public class ExamSessionServiceImpl implements ExamSessionService {
final Collection<APIMessage> result = new ArrayList<>();
final Exam exam = this.examDAO
.getWithQuizDataFromCache(examId)
.byPK(examId)
.getOrThrow();
// check lms connection

View file

@ -280,7 +280,7 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
checkReadPrivilege(institutionId);
return this.examDAO
.getWithQuizDataFromCache(modelId)
.byPK(modelId)
.flatMap(this.examAdminService::isRestricted)
.getOrThrow();
}