SEBSERV-417 fix check full integration available and fix Exam Config change apply
This commit is contained in:
parent
e0952da7f3
commit
b4907fcda9
11 changed files with 120 additions and 16 deletions
|
@ -18,6 +18,8 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.lms.FullLmsIntegrationServi
|
||||||
|
|
||||||
public interface FullLmsIntegrationAPI {
|
public interface FullLmsIntegrationAPI {
|
||||||
|
|
||||||
|
boolean fullIntegrationActive();
|
||||||
|
|
||||||
/** Performs a test for the underling {@link LmsSetup } configuration and checks if the
|
/** Performs a test for the underling {@link LmsSetup } configuration and checks if the
|
||||||
* LMS and the full LMS integration API of the LMS can be accessed or if there are some difficulties,
|
* LMS and the full LMS integration API of the LMS can be accessed or if there are some difficulties,
|
||||||
* missing API functions
|
* missing API functions
|
||||||
|
@ -34,4 +36,6 @@ public interface FullLmsIntegrationAPI {
|
||||||
Result<String> deleteConnectionDetails();
|
Result<String> deleteConnectionDetails();
|
||||||
|
|
||||||
Result<QuizData> getQuizDataForRemoteImport(String examData);
|
Result<QuizData> getQuizDataForRemoteImport(String examData);
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.impl.ExamDeletionEvent;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamTemplateChangeEvent;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamTemplateChangeEvent;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.LmsSetupChangeEvent;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.LmsSetupChangeEvent;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ConnectionConfigurationChangeEvent;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ConnectionConfigurationChangeEvent;
|
||||||
|
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamConfigUpdateEvent;
|
||||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
@ -37,6 +38,8 @@ public interface FullLmsIntegrationService {
|
||||||
void notifyConnectionConfigurationChange(ConnectionConfigurationChangeEvent event);
|
void notifyConnectionConfigurationChange(ConnectionConfigurationChangeEvent event);
|
||||||
@EventListener(ExamDeletionEvent.class)
|
@EventListener(ExamDeletionEvent.class)
|
||||||
void notifyExamDeletion(ExamDeletionEvent event);
|
void notifyExamDeletion(ExamDeletionEvent event);
|
||||||
|
@EventListener(ExamConfigUpdateEvent.class)
|
||||||
|
void notifyExamConfigChange(ExamConfigUpdateEvent event);
|
||||||
|
|
||||||
/** Applies the exam data to LMS to inform the LMS that the exam exists on SEB Server site.
|
/** Applies the exam data to LMS to inform the LMS that the exam exists on SEB Server site.
|
||||||
* @param exam The Exam
|
* @param exam The Exam
|
||||||
|
@ -75,6 +78,7 @@ public interface FullLmsIntegrationService {
|
||||||
String quizId,
|
String quizId,
|
||||||
AdHocAccountData adHocAccountData);
|
AdHocAccountData adHocAccountData);
|
||||||
|
|
||||||
|
|
||||||
final class AdHocAccountData {
|
final class AdHocAccountData {
|
||||||
public final String userId;
|
public final String userId;
|
||||||
public final String username;
|
public final String username;
|
||||||
|
|
|
@ -51,6 +51,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionService;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ConnectionConfigurationChangeEvent;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ConnectionConfigurationChangeEvent;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ConnectionConfigurationService;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ConnectionConfigurationService;
|
||||||
|
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamConfigUpdateEvent;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ScreenProctoringService;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ScreenProctoringService;
|
||||||
import org.apache.commons.io.IOUtils;
|
import org.apache.commons.io.IOUtils;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
@ -149,12 +150,10 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
|
||||||
@Override
|
@Override
|
||||||
public Result<Exam> applyExamDataToLMS(final Exam exam) {
|
public Result<Exam> applyExamDataToLMS(final Exam exam) {
|
||||||
return Result.tryCatch(() -> {
|
return Result.tryCatch(() -> {
|
||||||
final LmsSetup lmsSetup = lmsSetupDAO.byPK(exam.lmsSetupId).getOrThrow();
|
if (hasFullIntegration(exam.lmsSetupId)) {
|
||||||
if (lmsSetup.lmsType.features.contains(LmsSetup.Features.LMS_FULL_INTEGRATION)) {
|
|
||||||
this.applyExamData(exam, !exam.active);
|
this.applyExamData(exam, !exam.active);
|
||||||
this.applyConnectionConfiguration(exam);
|
this.applyConnectionConfiguration(exam);
|
||||||
}
|
}
|
||||||
|
|
||||||
return exam;
|
return exam;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -168,10 +167,29 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void notifyExamConfigChange(final ExamConfigUpdateEvent event) {
|
||||||
|
try {
|
||||||
|
|
||||||
|
final Exam exam = examDAO.byPK(event.examId).getOrThrow();
|
||||||
|
if (!hasFullIntegration(exam.lmsSetupId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.applyExamData(exam, !exam.active);
|
||||||
|
|
||||||
|
} catch (final Exception e) {
|
||||||
|
log.error(
|
||||||
|
"Failed to apply Exam Configuration change to fully integrated LMS for exam: {}",
|
||||||
|
event.examId,
|
||||||
|
e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void notifyLmsSetupChange(final LmsSetupChangeEvent event) {
|
public void notifyLmsSetupChange(final LmsSetupChangeEvent event) {
|
||||||
final LmsSetup lmsSetup = event.getLmsSetup();
|
final LmsSetup lmsSetup = event.getLmsSetup();
|
||||||
if (!lmsSetup.getLmsType().features.contains(LmsSetup.Features.LMS_FULL_INTEGRATION)) {
|
if (!hasFullIntegration(lmsSetup.id)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -206,6 +224,7 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
|
||||||
|
|
||||||
lmsSetupDAO.idsOfActiveWithFullIntegration(examTemplate.institutionId)
|
lmsSetupDAO.idsOfActiveWithFullIntegration(examTemplate.institutionId)
|
||||||
.onSuccess(all -> all.stream()
|
.onSuccess(all -> all.stream()
|
||||||
|
.filter(this::hasFullIntegration)
|
||||||
.map(this::applyFullLmsIntegration)
|
.map(this::applyFullLmsIntegration)
|
||||||
.forEach(res ->
|
.forEach(res ->
|
||||||
res.onError(error -> log.warn(
|
res.onError(error -> log.warn(
|
||||||
|
@ -229,15 +248,6 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
|
||||||
.forEach(this::applyConnectionConfiguration);
|
.forEach(this::applyConnectionConfiguration);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean needsConnectionConfigurationChange(final Exam exam, final Long ccId) {
|
|
||||||
if (exam.status == Exam.ExamStatus.ARCHIVED) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
final String configId = getConnectionConfigurationId(exam);
|
|
||||||
return StringUtils.isNotBlank(configId) && configId.equals(String.valueOf(ccId));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Result<IntegrationData> applyFullLmsIntegration(final Long lmsSetupId) {
|
public Result<IntegrationData> applyFullLmsIntegration(final Long lmsSetupId) {
|
||||||
return lmsSetupDAO
|
return lmsSetupDAO
|
||||||
|
@ -544,6 +554,9 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
|
||||||
}
|
}
|
||||||
|
|
||||||
private Exam applyExamData(final Exam exam, final boolean deletion) {
|
private Exam applyExamData(final Exam exam, final boolean deletion) {
|
||||||
|
if (!hasFullIntegration(exam.lmsSetupId)) {
|
||||||
|
return exam;
|
||||||
|
}
|
||||||
if (exam.examTemplateId == null) {
|
if (exam.examTemplateId == null) {
|
||||||
throw new IllegalStateException("Exam has no template id: " + exam.getName());
|
throw new IllegalStateException("Exam has no template id: " + exam.getName());
|
||||||
}
|
}
|
||||||
|
@ -611,6 +624,27 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
|
||||||
.getOr(exam);
|
.getOr(exam);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean hasFullIntegration(final Long lmsSetupId) {
|
||||||
|
final LmsAPITemplate lmsAPITemplate = this.lmsAPITemplateCacheService
|
||||||
|
.getLmsAPITemplate(lmsSetupId)
|
||||||
|
.getOrThrow();
|
||||||
|
final LmsSetup lmsSetup = lmsAPITemplate.lmsSetup();
|
||||||
|
if (!lmsSetup.getLmsType().features.contains(LmsSetup.Features.LMS_FULL_INTEGRATION)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return lmsAPITemplate.fullIntegrationActive();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean needsConnectionConfigurationChange(final Exam exam, final Long ccId) {
|
||||||
|
if (exam.status == Exam.ExamStatus.ARCHIVED) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final String configId = getConnectionConfigurationId(exam);
|
||||||
|
return StringUtils.isNotBlank(configId) && configId.equals(String.valueOf(ccId));
|
||||||
|
}
|
||||||
|
|
||||||
private String getAPIRootURL() {
|
private String getAPIRootURL() {
|
||||||
return webserviceInfo.getExternalServerURL() + lmsAPIEndpoint;
|
return webserviceInfo.getExternalServerURL() + lmsAPIEndpoint;
|
||||||
}
|
}
|
||||||
|
|
|
@ -500,6 +500,10 @@ public class LmsAPITemplateAdapter implements LmsAPITemplate {
|
||||||
return protectedRun;
|
return protectedRun;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean fullIntegrationActive() {
|
||||||
|
return this.lmsIntegrationAPI.fullIntegrationActive();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public LmsSetupTestResult testFullIntegrationAPI() {
|
public LmsSetupTestResult testFullIntegrationAPI() {
|
||||||
|
|
|
@ -422,6 +422,13 @@ public class AnsLmsAPITemplate extends AbstractCachedCourseAccess implements Lms
|
||||||
.map(x -> exam);
|
.map(x -> exam);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Full Integration API - Not integrated yet
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean fullIntegrationActive() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public LmsSetupTestResult testFullIntegrationAPI() {
|
public LmsSetupTestResult testFullIntegrationAPI() {
|
||||||
return LmsSetupTestResult.ofAPINotSupported(LmsType.ANS_DELFT);
|
return LmsSetupTestResult.ofAPINotSupported(LmsType.ANS_DELFT);
|
||||||
|
|
|
@ -19,6 +19,11 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.lms.FullLmsIntegrationServi
|
||||||
|
|
||||||
public class MockupFullIntegration implements FullLmsIntegrationAPI {
|
public class MockupFullIntegration implements FullLmsIntegrationAPI {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean fullIntegrationActive() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public LmsSetupTestResult testFullIntegrationAPI() {
|
public LmsSetupTestResult testFullIntegrationAPI() {
|
||||||
return LmsSetupTestResult.ofAPINotSupported(LmsSetup.LmsType.MOODLE_PLUGIN);
|
return LmsSetupTestResult.ofAPINotSupported(LmsSetup.LmsType.MOODLE_PLUGIN);
|
||||||
|
|
|
@ -64,6 +64,11 @@ public class MoodlePluginFullIntegration implements FullLmsIntegrationAPI {
|
||||||
Constants.TRUE_STRING));
|
Constants.TRUE_STRING));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean fullIntegrationActive() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public LmsSetupTestResult testFullIntegrationAPI() {
|
public LmsSetupTestResult testFullIntegrationAPI() {
|
||||||
final LmsSetupTestResult attributesCheck = this.restTemplateFactory.test();
|
final LmsSetupTestResult attributesCheck = this.restTemplateFactory.test();
|
||||||
|
@ -80,7 +85,12 @@ public class MoodlePluginFullIntegration implements FullLmsIntegrationAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
final MoodleAPIRestTemplate restTemplate = restTemplateRequest.get();
|
final MoodleAPIRestTemplate restTemplate = restTemplateRequest.get();
|
||||||
|
if (restTemplate.getMoodlePluginVersion() != MoodleAPIRestTemplate.MoodlePluginVersion.V2_0) {
|
||||||
|
throw new RuntimeException("Old Moodle Plugin Version: " + restTemplate.getMoodlePluginVersion().name());
|
||||||
|
}
|
||||||
|
|
||||||
restTemplate.testAPIConnection(
|
restTemplate.testAPIConnection(
|
||||||
FUNCTION_NAME_SEBSERVER_CONNECTION,
|
FUNCTION_NAME_SEBSERVER_CONNECTION,
|
||||||
FUNCTION_NAME_SEBSERVER_CONNECTION_DELETE,
|
FUNCTION_NAME_SEBSERVER_CONNECTION_DELETE,
|
||||||
|
|
|
@ -409,6 +409,13 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
|
||||||
.map(x -> exam);
|
.map(x -> exam);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Full Integration API - Not integrated yet
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean fullIntegrationActive() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public LmsSetupTestResult testFullIntegrationAPI() {
|
public LmsSetupTestResult testFullIntegrationAPI() {
|
||||||
return LmsSetupTestResult.ofAPINotSupported(LmsType.OPEN_OLAT);
|
return LmsSetupTestResult.ofAPINotSupported(LmsType.OPEN_OLAT);
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2019 ETH Zürich, IT Services
|
||||||
|
*
|
||||||
|
* 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.session;
|
||||||
|
|
||||||
|
import org.springframework.context.ApplicationEvent;
|
||||||
|
|
||||||
|
public class ExamConfigUpdateEvent extends ApplicationEvent {
|
||||||
|
|
||||||
|
public final Long examId;
|
||||||
|
public ExamConfigUpdateEvent(final Long examId) {
|
||||||
|
super(examId);
|
||||||
|
this.examId = examId;
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,9 +14,12 @@ import java.util.function.Function;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamConfigurationValueService;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamConfigurationValueService;
|
||||||
|
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.FullLmsIntegrationService;
|
||||||
|
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamConfigUpdateEvent;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
import org.springframework.context.annotation.Lazy;
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@ -49,6 +52,7 @@ public class ExamConfigUpdateServiceImpl implements ExamConfigUpdateService {
|
||||||
private final ExamUpdateHandler examUpdateHandler;
|
private final ExamUpdateHandler examUpdateHandler;
|
||||||
private final ExamAdminService examAdminService;
|
private final ExamAdminService examAdminService;
|
||||||
private final ExamConfigurationValueService examConfigurationValueService;
|
private final ExamConfigurationValueService examConfigurationValueService;
|
||||||
|
private final ApplicationEventPublisher applicationEventPublisher;
|
||||||
|
|
||||||
|
|
||||||
protected ExamConfigUpdateServiceImpl(
|
protected ExamConfigUpdateServiceImpl(
|
||||||
|
@ -58,7 +62,8 @@ public class ExamConfigUpdateServiceImpl implements ExamConfigUpdateService {
|
||||||
final ExamSessionService examSessionService,
|
final ExamSessionService examSessionService,
|
||||||
final ExamUpdateHandler examUpdateHandler,
|
final ExamUpdateHandler examUpdateHandler,
|
||||||
final ExamAdminService examAdminService,
|
final ExamAdminService examAdminService,
|
||||||
final ExamConfigurationValueService examConfigurationValueService) {
|
final ExamConfigurationValueService examConfigurationValueService,
|
||||||
|
final ApplicationEventPublisher applicationEventPublisher) {
|
||||||
|
|
||||||
this.examDAO = examDAO;
|
this.examDAO = examDAO;
|
||||||
this.configurationDAO = configurationDAO;
|
this.configurationDAO = configurationDAO;
|
||||||
|
@ -67,6 +72,7 @@ public class ExamConfigUpdateServiceImpl implements ExamConfigUpdateService {
|
||||||
this.examUpdateHandler = examUpdateHandler;
|
this.examUpdateHandler = examUpdateHandler;
|
||||||
this.examAdminService = examAdminService;
|
this.examAdminService = examAdminService;
|
||||||
this.examConfigurationValueService = examConfigurationValueService;
|
this.examConfigurationValueService = examConfigurationValueService;
|
||||||
|
this.applicationEventPublisher = applicationEventPublisher;
|
||||||
}
|
}
|
||||||
|
|
||||||
// processing:
|
// processing:
|
||||||
|
@ -102,7 +108,7 @@ public class ExamConfigUpdateServiceImpl implements ExamConfigUpdateService {
|
||||||
final Collection<Exam> exams = lockForUpdate(examIdsFirstCheck, updateId)
|
final Collection<Exam> exams = lockForUpdate(examIdsFirstCheck, updateId)
|
||||||
.stream()
|
.stream()
|
||||||
.map(Result::getOrThrow)
|
.map(Result::getOrThrow)
|
||||||
.collect(Collectors.toList());
|
.toList();
|
||||||
|
|
||||||
final Collection<Long> examsIds = exams
|
final Collection<Long> examsIds = exams
|
||||||
.stream()
|
.stream()
|
||||||
|
@ -243,6 +249,9 @@ public class ExamConfigUpdateServiceImpl implements ExamConfigUpdateService {
|
||||||
.releaseLock(exam.id, updateId)
|
.releaseLock(exam.id, updateId)
|
||||||
.onError(t -> log.error("Failed to release lock for exam: {}", exam));
|
.onError(t -> log.error("Failed to release lock for exam: {}", exam));
|
||||||
|
|
||||||
|
// notify...
|
||||||
|
applicationEventPublisher.publishEvent(new ExamConfigUpdateEvent(exam.id));
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
})
|
})
|
||||||
.onError(t -> this.examDAO.forceUnlock(mapping.examId));
|
.onError(t -> this.examDAO.forceUnlock(mapping.examId));
|
||||||
|
|
|
@ -193,7 +193,7 @@ public class APIExceptionHandler extends ResponseEntityExceptionHandler {
|
||||||
final PermissionDeniedException ex,
|
final PermissionDeniedException ex,
|
||||||
final WebRequest request) {
|
final WebRequest request) {
|
||||||
|
|
||||||
log.info("Permission Denied Exception: ", ex);
|
log.info("Permission Denied Exception: {}", ex.getMessage());
|
||||||
return APIMessage.ErrorMessage.FORBIDDEN
|
return APIMessage.ErrorMessage.FORBIDDEN
|
||||||
.createErrorResponse(ex.getMessage());
|
.createErrorResponse(ex.getMessage());
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue