improved SEB Restriction handling error handling and logging

This commit is contained in:
anhefti 2023-12-14 12:12:49 +01:00
parent 0544d7a799
commit fc310597e6
19 changed files with 77 additions and 139 deletions

View file

@ -177,9 +177,7 @@ public final class CircuitBreaker<T> {
this.state = State.HALF_OPEN;
this.failingCount.set(0);
return Result.ofError(new RuntimeException(
"Set CircuitBeaker to half-open state. Cause: " + result.getError(),
result.getError()));
return result;
} else {
// try again
return protectedRun(supplier);
@ -258,7 +256,7 @@ public final class CircuitBreaker<T> {
return Result.ofError(e);
} catch (final ExecutionException e) {
future.cancel(false);
if (log.isWarnEnabled()) {
if (log.isDebugEnabled()) {
log.warn("Attempt error: {}, {}", e.getMessage(), this.state);
}
final Throwable cause = e.getCause();

View file

@ -142,7 +142,8 @@ public final class Result<T> {
if (this.error instanceof RuntimeException) {
throw (RuntimeException) this.error;
} else {
throw new RuntimeException("RuntimeExceptionWrapper cause: " + this.error.getMessage(), this.error);
String cause = this.error.getMessage() != null ? this.error.getMessage() : this.error.toString();
throw new RuntimeException("RuntimeExceptionWrapper cause: " + cause, this.error);
}
}

View file

@ -238,7 +238,7 @@ public class ExamForm implements TemplateComposer {
.withURIVariable(API.PARAM_MODEL_ID, exam.getModelId())
.call()
.onError(e -> log.error("Unexpected error while trying to verify seb restriction settings: ", e))
.getOr(false);
.getOr(exam.sebRestriction);
final boolean sebRestrictionMismatch = readonly &&
sebRestrictionAvailable &&
isRestricted != exam.sebRestriction &&
@ -525,7 +525,6 @@ public class ExamForm implements TemplateComposer {
final boolean newExam = exam.id == null;
final boolean hasLMS = exam.lmsSetupId != null;
final boolean importFromLMS = newExam && hasLMS;
final DateTimeZone timeZone = this.pageService.getCurrentUser().get().timeZone;
final LocTextKey statusTitle = new LocTextKey("sebserver.exam.status." + exam.status.name());
return this.pageService.formBuilder(formContext.copyOf(content))
@ -585,9 +584,7 @@ public class ExamForm implements TemplateComposer {
exam.getDescription())
.asArea()
.readonly(hasLMS))
.withAdditionalValueMapping(
QuizData.QUIZ_ATTR_DESCRIPTION,
QuizData.QUIZ_ATTR_DESCRIPTION)
.withAdditionalValueMapping(QuizData.QUIZ_ATTR_DESCRIPTION)
.addField(FormBuilder.dateTime(
Domain.EXAM.ATTR_QUIZ_START_TIME,
@ -599,11 +596,8 @@ public class ExamForm implements TemplateComposer {
.addField(FormBuilder.dateTime(
Domain.EXAM.ATTR_QUIZ_END_TIME,
FORM_END_TIME_TEXT_KEY,
exam.endTime != null
? exam.endTime
: DateTime.now(timeZone).plusHours(1))
.readonly(hasLMS)
.mandatory(!hasLMS))
exam.endTime)
.readonly(hasLMS))
.addField(FormBuilder.text(
QuizData.QUIZ_ATTR_START_URL,
@ -611,9 +605,7 @@ public class ExamForm implements TemplateComposer {
exam.getStartURL())
.readonly(hasLMS)
.mandatory(!hasLMS))
.withAdditionalValueMapping(
QuizData.QUIZ_ATTR_START_URL,
QuizData.QUIZ_ATTR_START_URL)
.withAdditionalValueMapping(QuizData.QUIZ_ATTR_START_URL)
.addField(FormBuilder.singleSelection(
Domain.EXAM.ATTR_TYPE,
@ -636,6 +628,7 @@ public class ExamForm implements TemplateComposer {
}
private Exam newExamNoLMS() {
final DateTimeZone timeZone = this.pageService.getCurrentUser().get().timeZone;
return new Exam(
null,
this.pageService.getCurrentUser().get().institutionId,
@ -643,8 +636,8 @@ public class ExamForm implements TemplateComposer {
UUID.randomUUID().toString(),
true,
null,
null,
null,
DateTime.now(timeZone),
DateTime.now(timeZone).plusHours(1),
Exam.ExamType.UNDEFINED,
null,
null,

View file

@ -23,10 +23,11 @@ public class DateTimeSelectorFieldBuilder extends FieldBuilder<DateTime> {
final Composite fieldGrid = createFieldGrid(builder.formParent, this.spanInput);
if (readonly) {
final Text label = new Text(fieldGrid, SWT.NONE);
label.setText(builder.i18nSupport.formatDisplayDateTime(value) + " " + builder.i18nSupport.getUsersTimeZoneTitleSuffix());
label.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, true));
builder.form.putReadonlyField(this.name, titleLabel, label);
final Text readonlyLabel = builder.widgetFactory.textInput(fieldGrid, this.label);
readonlyLabel.setEditable(false);
readonlyLabel.setText(builder.i18nSupport.formatDisplayDateWithTimeZone(value));
readonlyLabel.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, true));
builder.form.putReadonlyField(this.name, titleLabel, readonlyLabel);
return;
}

View file

@ -16,7 +16,6 @@ import java.util.function.Predicate;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gui.widget.*;
import com.fasterxml.jackson.databind.JsonNode;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.rap.rwt.RWT;
@ -105,6 +104,10 @@ public final class Form implements FormBinding {
this.additionalAttributeMapping.put(fieldName, attrName);
}
public void putAdditionalValueMapping(final String fieldName) {
this.additionalAttributeMapping.put(fieldName, fieldName);
}
public String getStaticValue(final String name) {
return this.staticValues.get(name);
}
@ -188,7 +191,7 @@ public final class Form implements FormBinding {
Form removeField(final String name) {
if (this.formFields.containsKey(name)) {
final List<FormFieldAccessor> list = this.formFields.remove(name);
list.forEach(ffa -> ffa.dispose());
list.forEach(FormFieldAccessor::dispose);
}
return this;
@ -318,7 +321,7 @@ public final class Form implements FormBinding {
additionalAttrs.put(entry.getValue(), fieldValue);
}
}
if (additionalAttrs != null) {
if (!additionalAttrs.isEmpty()) {
this.objectRoot.putIfAbsent(
API.PARAM_ADDITIONAL_ATTRIBUTES,
jsonMapper.valueToTree(additionalAttrs));

View file

@ -152,6 +152,21 @@ public class FormBuilder {
return this;
}
public FormBuilder withAdditionalValueMapping(final String fieldName) {
this.form.putAdditionalValueMapping(fieldName);
return this;
}
public FormBuilder withAdditionalValueMappingIf(
final BooleanSupplier condition,
final Supplier<String> fieldNameSupplier) {
if (condition.getAsBoolean()) {
this.form.putAdditionalValueMapping(fieldNameSupplier.get());
}
return this;
}
public FormBuilder addFieldIf(
final BooleanSupplier condition,
final Supplier<FieldBuilder<?>> templateSupplier) {

View file

@ -11,6 +11,7 @@ package ch.ethz.seb.sebserver.gui.service.i18n;
import java.util.Collection;
import java.util.Locale;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import ch.ethz.seb.sebserver.gbl.util.Utils;
@ -61,7 +62,9 @@ public interface I18nSupport {
* @param date the DateTime instance
* @return date formatted date String to display */
default String formatDisplayDateWithTimeZone(final DateTime date) {
return formatDisplayDateTime(date) + " " + this.getUsersTimeZoneTitleSuffix();
return formatDisplayDateTime(date) + (date != null
? " " + this.getUsersTimeZoneTitleSuffix()
: StringUtils.EMPTY);
}
/** Format a time-stamp (milliseconds) to a text format to display.

View file

@ -124,7 +124,7 @@ public class ExamAdminServiceImpl implements ExamAdminService {
EntityType.EXAM,
examId,
Exam.ADDITIONAL_ATTR_SIGNATURE_KEY_SALT,
KeyGenerators.string().generateKey().toString());
KeyGenerators.string().generateKey());
return exam;
}).flatMap(this::initAdditionalAttributesForMoodleExams);
@ -204,7 +204,7 @@ public class ExamAdminServiceImpl implements ExamAdminService {
return this.lmsAPIService
.getLmsAPITemplate(exam.lmsSetupId)
.map(lmsAPI -> lmsAPI.hasSEBClientRestriction(exam))
.onError(error -> log.error("Failed to check SEB restriction: ", error));
.onError(error -> log.warn("Failed to check SEB restriction: {}", error.getMessage()));
}
@Override
@ -301,7 +301,7 @@ public class ExamAdminServiceImpl implements ExamAdminService {
return getProctoringServiceSettings(exam.id)
.map(settings -> {
ProctoringServiceSettings resetSettings;
final ProctoringServiceSettings resetSettings;
if (exam.examTemplateId != null) {
// get settings from origin template
resetSettings = this.proctoringAdminService
@ -401,7 +401,7 @@ public class ExamAdminServiceImpl implements ExamAdminService {
.stream()
.forEach(configNodeId -> {
if (this.examConfigurationMapDAO.checkNoActiveExamReferences(configNodeId).getOr(false)) {
log.debug("Also set exam configuration to archived: ", configNodeId);
log.debug("Also set exam configuration to archived: {}", configNodeId);
this.configurationNodeDAO.save(
new ConfigurationNode(
configNodeId, null, null, null, null, null,

View file

@ -23,18 +23,18 @@ public interface SEBRestrictionAPI {
* @return {@link LmsSetupTestResult } instance with the test result report */
LmsSetupTestResult testCourseRestrictionAPI();
/** Get SEB restriction data from LMS within a {@link SEBRestrictionData } instance. The available restriction
/** Get SEB restriction data from LMS within a {@link SEBRestriction } instance. The available restriction
* details
* depends on the type of LMS but shall at least contains the config-key(s) and the browser-exam-key(s).
*
* @param exam the exam to get the SEB restriction data for
* @return Result refer to the {@link SEBRestrictionData } instance or to an ResourceNotFoundException if the
* @return Result refer to the {@link SEBRestriction } instance or to an ResourceNotFoundException if the
* restriction is
* missing or to another exception on unexpected error case */
Result<SEBRestriction> getSEBClientRestriction(Exam exam);
/** Use this to check if there is a SEB restriction available on the LMS for the specified exam.
*
* <p>
* A SEB Restriction is available if it can get from LMS and if there is either a Config-Key
* or a BrowserExam-Key set or both. If none of this keys is set, the SEB Restriction is been
* considered to not set on the LMS.
@ -42,12 +42,7 @@ public interface SEBRestrictionAPI {
* @param exam exam the exam to get the SEB restriction data for
* @return true if there is a SEB restriction set on the LMS for the exam or false otherwise */
default boolean hasSEBClientRestriction(final Exam exam) {
final Result<SEBRestriction> sebClientRestriction = getSEBClientRestriction(exam);
if (sebClientRestriction.hasError()) {
return false;
}
return hasSEBClientRestriction(sebClientRestriction.get());
return hasSEBClientRestriction(getSEBClientRestriction(exam).getOrThrow());
}
default boolean hasSEBClientRestriction(final SEBRestriction sebRestriction) {
@ -58,7 +53,7 @@ public interface SEBRestrictionAPI {
*
* @param exam The exam to apply the restriction for
* @param sebRestrictionData containing all data for SEB Client restriction to apply to the LMS
* @return Result refer to the given {@link SEBRestrictionData } if restriction was successful or to an error if
* @return Result refer to the given {@link SEBRestriction } if restriction was successful or to an error if
* not */
Result<SEBRestriction> applySEBClientRestriction(
Exam exam,

View file

@ -153,8 +153,11 @@ public class LmsAPIServiceImpl implements LmsAPIService {
}
if (template.lmsSetup().getLmsType().features.contains(LmsSetup.Features.SEB_RESTRICTION)) {
this.cache.remove(new CacheKey(template.lmsSetup().getModelId(), 0));
return template.testCourseRestrictionAPI();
final LmsSetupTestResult lmsSetupTestResult = template.testCourseRestrictionAPI();
if (!lmsSetupTestResult.isOk()) {
this.cache.remove(new CacheKey(template.lmsSetup().getModelId(), 0));
}
return lmsSetupTestResult;
}
return LmsSetupTestResult.ofOkay(template.lmsSetup().getLmsType());

View file

@ -386,18 +386,12 @@ public class LmsAPITemplateAdapter implements LmsAPITemplate {
return this.restrictionRequest.protectedRun(() -> this.sebRestrictionAPI
.getSEBClientRestriction(exam)
.onError(error -> log.error("Failed to get SEB restrictions: {}", error.getMessage()))
.getOrThrow());
}
@Override
public boolean hasSEBClientRestriction(final Exam exam) {
final Result<SEBRestriction> sebClientRestriction = getSEBClientRestriction(exam);
if (sebClientRestriction.hasError()) {
return false;
}
return this.sebRestrictionAPI.hasSEBClientRestriction(sebClientRestriction.get());
return this.sebRestrictionAPI.hasSEBClientRestriction(getSEBClientRestriction(exam).getOrThrow());
}
@Override

View file

@ -18,6 +18,7 @@ import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.AdditionalAttributeRecord;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -102,7 +103,7 @@ public class SEBRestrictionServiceImpl implements SEBRestrictionService {
.getOrThrow();
configKeys.addAll(generatedKeys);
if (generatedKeys != null && !generatedKeys.isEmpty()) {
if (!generatedKeys.isEmpty()) {
configKeys.addAll(this.lmsAPIService
.getLmsAPITemplate(exam.lmsSetupId)
.flatMap(lmsTemplate -> lmsTemplate.getSEBClientRestriction(exam))
@ -130,8 +131,8 @@ public class SEBRestrictionServiceImpl implements SEBRestrictionService {
.collect(Collectors.toMap(
attr -> attr.getName().replace(
SEB_RESTRICTION_ADDITIONAL_PROPERTY_NAME_PREFIX,
""),
attr -> attr.getValue())));
StringUtils.EMPTY),
AdditionalAttributeRecord::getValue)));
} catch (final Exception e) {
log.error(
"Failed to load additional SEB restriction properties from AdditionalAttributes of the Exam: {}",

View file

@ -8,6 +8,10 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.mockup;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -23,6 +27,8 @@ public class MockSEBRestrictionAPI implements SEBRestrictionAPI {
private static final Logger log = LoggerFactory.getLogger(MockSEBRestrictionAPI.class);
//private Map<Long, Boolean> restrictionDB = new ConcurrentHashMap<>();
@Override
public LmsSetupTestResult testCourseRestrictionAPI() {
return LmsSetupTestResult.ofOkay(LmsType.MOCKUP);
@ -31,15 +37,6 @@ public class MockSEBRestrictionAPI implements SEBRestrictionAPI {
@Override
public Result<SEBRestriction> getSEBClientRestriction(final Exam exam) {
log.info("Get SEB Client restriction for Exam: {}", exam);
// if (BooleanUtils.toBoolean(exam.sebRestriction)) {
// return Result.of(new SEBRestriction(
// exam.id,
// Stream.of("configKey").collect(Collectors.toList()),
// Collections.emptyList(),
// Collections.emptyMap()));
// } else {
// return Result.ofError(new NoSEBRestrictionException());
// }
return Result.ofError(new NoSEBRestrictionException());
}

View file

@ -164,7 +164,6 @@ public class MoodleRestTemplateFactoryImpl implements MoodleRestTemplateFactory
public Result<MoodleAPIRestTemplate> createRestTemplate(final String service, final String accessTokenPath) {
final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup();
return Result.tryCatch(() -> {
final ClientCredentials credentials = this.apiTemplateDataSupplier.getLmsClientCredentials();
final ProxyData proxyData = this.apiTemplateDataSupplier.getProxyData();
@ -319,7 +318,7 @@ public class MoodleRestTemplateFactoryImpl implements MoodleRestTemplateFactory
}
final boolean usePOST = queryAttributes != null && !queryAttributes.isEmpty();
HttpEntity<?> functionReqEntity;
final HttpEntity<?> functionReqEntity;
if (usePOST) {
final HttpHeaders headers = new HttpHeaders();
headers.set(
@ -401,8 +400,7 @@ public class MoodleRestTemplateFactoryImpl implements MoodleRestTemplateFactory
if (moodleToken == null || moodleToken.token == null) {
throw new RuntimeException("Access Token request with 200 but no or invalid token body");
} else {
log.info("Successfully get access token from Moodle: {}",
lmsSetup);
log.info("Successfully get access token from Moodle: {}", lmsSetup.name);
}
this.accessToken = moodleToken.token;

View file

@ -1,68 +0,0 @@
/*
* Copyright (c) 2022 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.plugin;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleAPIRestTemplate;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory;
@Lazy
@Service
@WebServiceProfile
public class MoodlePluginCheck {
private static final Logger log = LoggerFactory.getLogger(MoodlePluginCheck.class);
/** Used to check if the moodle SEB Server plugin is available for a given LMSSetup.
*
* @param lmsSetup The LMS Setup
* @return true if the SEB Server plugin is available */
public boolean checkPluginAvailable(final MoodleRestTemplateFactory restTemplateFactory) {
try {
log.info("Check Moodle SEB Server Plugin available...");
final LmsSetupTestResult test = restTemplateFactory.test();
if (!test.isOk()) {
log.warn("Failed to check Moodle SEB Server Plugin because of invalid LMS Setup: ", test);
return false;
}
final MoodleAPIRestTemplate restTemplate = restTemplateFactory
.createRestTemplate(MooldePluginLmsAPITemplateFactory.SEB_SERVER_SERVICE_NAME)
.getOrThrow();
try {
restTemplate.testAPIConnection(
MoodlePluginCourseAccess.COURSES_API_FUNCTION_NAME,
MoodlePluginCourseAccess.USERS_API_FUNCTION_NAME);
} catch (final Exception e) {
log.info("Moodle SEB Server Plugin not available: {}", e.getMessage());
return false;
}
log.info("Moodle SEB Server Plugin not available for: {}",
restTemplateFactory.getApiTemplateDataSupplier().getLmsSetup());
return true;
} catch (final Exception e) {
log.error("Failed to check Moodle SEB Server Plugin because of unexpected error: ", e);
return false;
}
}
}

View file

@ -109,6 +109,7 @@ public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess impleme
final boolean applyNameCriteria) {
super(cacheManager);
this.jsonMapper = jsonMapper;
this.restTemplateFactory = restTemplateFactory;
this.applyNameCriteria = applyNameCriteria;
@ -594,7 +595,9 @@ public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess impleme
}
private Result<MoodleAPIRestTemplate> getRestTemplate() {
if (this.restTemplate == null) {
final Result<MoodleAPIRestTemplate> templateRequest = this.restTemplateFactory
.createRestTemplate(MooldePluginLmsAPITemplateFactory.SEB_SERVER_SERVICE_NAME);
if (templateRequest.hasError()) {

View file

@ -283,13 +283,13 @@ public class MoodlePluginCourseRestriction implements SEBRestrictionAPI {
private Result<MoodleAPIRestTemplate> getRestTemplate() {
if (this.restTemplate == null) {
final Result<MoodleAPIRestTemplate> templateRequest = this.restTemplateFactory
.createRestTemplate(MooldePluginLmsAPITemplateFactory.SEB_SERVER_SERVICE_NAME);
if (templateRequest.hasError()) {
return templateRequest;
} else {
final MoodleAPIRestTemplate moodleAPIRestTemplate = templateRequest.get();
this.restTemplate = moodleAPIRestTemplate;
this.restTemplate = templateRequest.get();
}
}

View file

@ -337,7 +337,7 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
return this.examDAO
.byPK(modelId)
.flatMap(this.examAdminService::isRestricted)
.getOrThrow();
.getOr(false);
}
@RequestMapping(
@ -376,7 +376,7 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
return this.entityDAO.byPK(examId)
.flatMap(this.authorization::checkModify)
.flatMap(exam -> this.sebRestrictionService.saveSEBRestrictionToExam(exam, sebRestriction))
.flatMap(exam -> this.examAdminService.isRestricted(exam).getOrThrow()
.flatMap(exam -> this.examAdminService.isRestricted(exam).getOr(false)
? this.applySEBRestriction(exam, true)
: Result.of(exam))
.getOrThrow();

View file

@ -33,6 +33,7 @@ public class MoodlePluginCourseRestrictionTest {
@Test
public void testSetup() {
final MoodlePluginCourseRestriction candidate = crateMockup();
assertEquals("MoodlePluginCourseRestriction [restTemplate=null]", candidate.toTestString());