refactoring, добавление портов, разделение домена и реализацию

This commit is contained in:
Anton Dzyk 2026-01-11 12:50:55 +03:00
parent bf68f3fe0a
commit 498ba86305
44 changed files with 585 additions and 405 deletions

View File

@ -5,31 +5,26 @@ import jakarta.servlet.http.HttpSession;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import ru.oa2.lti.application.infrastructure.lms.LMSService; import ru.oa2.lti.application.usecase.lti.HandleLtiRedirectUseCase;
import ru.oa2.lti.application.service.history.HistoryService; import ru.oa2.lti.application.usecase.lti.ProcessLtiLoginUseCase;
import ru.oa2.lti.application.service.jwt.JwtService;
import ru.oa2.lti.domain.model.auth.LtiLogin;
/**
/* * Входная точка в external tool по протоколу LTI 1.3.
Входная точка в external tool по протоколу LTI 1.3 *
* Контроллер только обрабатывает HTTP запросы и делегирует
#TODO расписать процесс и добавить линк на доку * бизнес-логику в Use Cases.
*/ */
@Controller @Controller
@RequestMapping("/tool/lti") @RequestMapping("/tool/lti")
public class LaunchController { public class LaunchController {
private final JwtService jwtService; private final ProcessLtiLoginUseCase processLtiLoginUseCase;
private final LMSService lmsService; private final HandleLtiRedirectUseCase handleLtiRedirectUseCase;
private final HistoryService historyService;
public LaunchController(JwtService jwtService, public LaunchController(ProcessLtiLoginUseCase processLtiLoginUseCase,
LMSService lmsService, HandleLtiRedirectUseCase handleLtiRedirectUseCase) {
HistoryService historyService) { this.processLtiLoginUseCase = processLtiLoginUseCase;
this.jwtService = jwtService; this.handleLtiRedirectUseCase = handleLtiRedirectUseCase;
this.lmsService = lmsService;
this.historyService = historyService;
} }
@ResponseBody @ResponseBody
@ -42,43 +37,31 @@ public class LaunchController {
@RequestParam("target_link_uri") String targetLinkUri, @RequestParam("target_link_uri") String targetLinkUri,
HttpServletRequest request) { HttpServletRequest request) {
var ltiLogin = LtiLogin.builder() // Создаём команду для Use Case
.clientId(clientId) var command = new ProcessLtiLoginUseCase.LtiLoginCommand(
.iss(iss) clientId, iss, loginHint, ltiDeploymentId, ltiMessageHint, targetLinkUri
.loginHint(loginHint)
.ltiDeploymentId(ltiDeploymentId)
.ltiMessageHint(ltiMessageHint)
.targetLinkUri(targetLinkUri)
.build();
var payload = jwtService.getPayload(ltiLogin.getLoginHint());
// 4. Сохранить access token в сессии
HttpSession session = request.getSession();
session.setAttribute("lti_login", ltiLogin);
session.setAttribute("lti_message_hint", Long.valueOf(ltiMessageHint));
session.setAttribute("lti_context_id", payload.getContextId());
session.setAttribute("lti_user_id", ltiLogin.getClientId());
session.setAttribute("payload", payload);
// Логирование события входа
historyService.logAction(
payload.getDeploymentId(),
payload.getContextId(),
"LTI_LOGIN",
String.format("User login via LTI 1.3, clientId=%s", clientId)
); );
if (payload.getContextId() != null) { // Выполняем бизнес-логику через Use Case
return lmsService.ltiAuth(ltiMessageHint, iss, loginHint); var result = processLtiLoginUseCase.execute(command);
}
// Сохраняем данные в сессии (HTTP-специфичная логика)
HttpSession session = request.getSession();
session.setAttribute("lti_login", result.ltiLogin());
session.setAttribute("lti_message_hint", Long.valueOf(ltiMessageHint));
session.setAttribute("lti_context_id", result.payload().getContextId());
session.setAttribute("lti_user_id", clientId);
session.setAttribute("payload", result.payload());
// Возвращаем результат
if (result.requiresSelection()) {
return "redirect:/tool/lti/select"; return "redirect:/tool/lti/select";
} }
return result.redirectResponse();
}
@GetMapping("/select") @GetMapping("/select")
public String select(Model model) { public String select(Model model) {
//TODO доработать форму выбора существующих заданий
return "select"; return "select";
} }
@ -87,21 +70,14 @@ public class LaunchController {
@RequestParam("state") String state, @RequestParam("state") String state,
HttpServletRequest request) { HttpServletRequest request) {
// Выполняем бизнес-логику через Use Case
var result = handleLtiRedirectUseCase.execute(idToken, state);
// Сохраняем данные в сессии (HTTP-специфичная логика)
HttpSession session = request.getSession(); HttpSession session = request.getSession();
session.setAttribute("id_token", idToken); session.setAttribute("id_token", idToken);
session.setAttribute("state", state); session.setAttribute("state", state);
// Декодируем id_token для получения информации о контексте return "redirect:" + result.redirectUrl();
var idTokenPayload = jwtService.getTokenPayload(idToken);
// Логирование перенаправления после авторизации
historyService.logAction(
idTokenPayload.getDeploymentId(),
idTokenPayload.getContext() != null ? idTokenPayload.getContext().id() : null,
"LTI_REDIRECT",
"User redirected after OAuth authentication"
);
return "redirect:/tool/lti/task";
} }
} }

View File

@ -10,14 +10,14 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import ru.oa2.lti.application.service.history.HistoryService; import ru.oa2.lti.domain.port.out.HistoryRepository;
import ru.oa2.lti.application.service.task.TaskService; import ru.oa2.lti.application.service.task.TaskService;
import ru.oa2.lti.domain.model.ResultRequest; import ru.oa2.lti.application.dto.auth.LtiLogin;
import ru.oa2.lti.domain.model.auth.LtiLogin; import ru.oa2.lti.application.dto.auth.Payload;
import ru.oa2.lti.domain.model.auth.Payload; import ru.oa2.lti.application.dto.result.ResultResponse;
import ru.oa2.lti.domain.model.results.ResultResponse; import ru.oa2.lti.application.dto.task.CheckTaskRequest;
import ru.oa2.lti.domain.model.task.RequestUpdateTask; import ru.oa2.lti.application.dto.task.RequestUpdateTask;
import ru.oa2.lti.domain.model.task.TaskData; import ru.oa2.lti.application.dto.task.TaskData;
import java.util.Objects; import java.util.Objects;
@ -34,11 +34,11 @@ POST /submit - отправка на автоматическую проверк
public class TaskController { public class TaskController {
private final TaskService taskService; private final TaskService taskService;
private final HistoryService historyService; private final HistoryRepository historyRepository;
public TaskController(TaskService taskService, HistoryService historyService) { public TaskController(TaskService taskService, HistoryRepository historyRepository) {
this.taskService = taskService; this.taskService = taskService;
this.historyService = historyService; this.historyRepository = historyRepository;
} }
@GetMapping @GetMapping
@ -52,7 +52,7 @@ public class TaskController {
var data = taskService.getTask(payload.getContextId()); var data = taskService.getTask(payload.getContextId());
if (data == null) { if (data == null) {
historyService.logAction( historyRepository.logAction(
payload.getDeploymentId(), payload.getDeploymentId(),
payload.getContextId(), payload.getContextId(),
"TASK_ERROR", "TASK_ERROR",
@ -65,7 +65,7 @@ public class TaskController {
model.addAttribute("description", data.getDescription()); model.addAttribute("description", data.getDescription());
// Логирование просмотра задачи // Логирование просмотра задачи
historyService.logAction( historyRepository.logAction(
payload.getDeploymentId(), payload.getDeploymentId(),
payload.getContextId(), payload.getContextId(),
"TASK_VIEW", "TASK_VIEW",
@ -93,7 +93,7 @@ public class TaskController {
if (payload != null && payload.getCoach()) { if (payload != null && payload.getCoach()) {
// Логирование обновления задачи // Логирование обновления задачи
historyService.logAction( historyRepository.logAction(
payload.getDeploymentId(), payload.getDeploymentId(),
payload.getContextId(), payload.getContextId(),
"TASK_UPDATE", "TASK_UPDATE",
@ -110,7 +110,7 @@ public class TaskController {
// Логирование попытки несанкционированного обновления // Логирование попытки несанкционированного обновления
if (payload != null) { if (payload != null) {
historyService.logAction( historyRepository.logAction(
payload.getDeploymentId(), payload.getDeploymentId(),
payload.getContextId(), payload.getContextId(),
"TASK_UPDATE_DENIED", "TASK_UPDATE_DENIED",
@ -134,7 +134,7 @@ public class TaskController {
// Логирование отправки задачи на проверку // Логирование отправки задачи на проверку
if (payload != null) { if (payload != null) {
historyService.logAction( historyRepository.logAction(
payload.getDeploymentId(), payload.getDeploymentId(),
payload.getContextId(), payload.getContextId(),
"TASK_SUBMIT", "TASK_SUBMIT",
@ -144,6 +144,6 @@ public class TaskController {
return ResponseEntity return ResponseEntity
.accepted() .accepted()
.body(taskService.checkTask(new ResultRequest(ltiLogin.getClientId(), idToken))); .body(taskService.checkTask(new CheckTaskRequest(ltiLogin.getClientId(), idToken)));
} }
} }

View File

@ -1,5 +0,0 @@
package ru.oa2.lti.application.domain.carecase;
public class GetTask {
}

View File

@ -1,4 +1,4 @@
package ru.oa2.lti.domain.model.auth; package ru.oa2.lti.application.dto.auth;
import lombok.Data; import lombok.Data;

View File

@ -1,4 +1,4 @@
package ru.oa2.lti.domain.model.auth; package ru.oa2.lti.application.dto.auth;
/* /*
"https://purl.imsglobal.org/spec/lti/claim/launch_presentation" : { "https://purl.imsglobal.org/spec/lti/claim/launch_presentation" : {

View File

@ -1,4 +1,4 @@
package ru.oa2.lti.domain.model.auth; package ru.oa2.lti.application.dto.auth;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;

View File

@ -1,4 +1,4 @@
package ru.oa2.lti.domain.model.auth; package ru.oa2.lti.application.dto.auth;
import java.util.List; import java.util.List;

View File

@ -1,4 +1,4 @@
package ru.oa2.lti.domain.model.auth; package ru.oa2.lti.application.dto.auth;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;

View File

@ -1,4 +1,4 @@
package ru.oa2.lti.domain.model.auth; package ru.oa2.lti.application.dto.auth;
import java.util.UUID; import java.util.UUID;

View File

@ -1,4 +1,4 @@
package ru.oa2.lti.domain.model.auth; package ru.oa2.lti.application.dto.auth;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;

View File

@ -1,4 +1,4 @@
package ru.oa2.lti.domain.model.auth; package ru.oa2.lti.application.dto.auth;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;

View File

@ -1,4 +1,4 @@
package ru.oa2.lti.domain.model.auth; package ru.oa2.lti.application.dto.auth;
import java.util.List; import java.util.List;

View File

@ -1,4 +1,4 @@
package ru.oa2.lti.domain.model.results; package ru.oa2.lti.application.dto.result;
public enum GradingType { public enum GradingType {
NotStarted, NotStarted,

View File

@ -1,4 +1,4 @@
package ru.oa2.lti.domain.model.results; package ru.oa2.lti.application.dto.result;
public record Lineitems( public record Lineitems(
String id, String id,

View File

@ -1,4 +1,4 @@
package ru.oa2.lti.domain.model.results; package ru.oa2.lti.application.dto.result;
public enum ProgressType { public enum ProgressType {
Initialized, Initialized,

View File

@ -1,4 +1,4 @@
package ru.oa2.lti.domain.model.results; package ru.oa2.lti.application.dto.result;
import lombok.Builder; import lombok.Builder;

View File

@ -1,4 +1,4 @@
package ru.oa2.lti.domain.model.results; package ru.oa2.lti.application.dto.result;
public record ResultResponse( public record ResultResponse(
String status String status

View File

@ -0,0 +1,9 @@
package ru.oa2.lti.application.dto.task;
/**
* DTO для запроса проверки задачи
*/
public record CheckTaskRequest(
String clientId,
String idToken
) {}

View File

@ -1,4 +1,4 @@
package ru.oa2.lti.domain.model.task; package ru.oa2.lti.application.dto.task;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;

View File

@ -1,4 +1,4 @@
package ru.oa2.lti.domain.model.task; package ru.oa2.lti.application.dto.task;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;

View File

@ -1,4 +1,4 @@
package ru.oa2.lti.domain.model.task; package ru.oa2.lti.application.dto.task;
public enum TaskQueueStatus { public enum TaskQueueStatus {
NEW, NEW,

View File

@ -1,4 +1,4 @@
package ru.oa2.lti.domain.model.task; package ru.oa2.lti.application.dto.task;
public record TaskResult( public record TaskResult(
//TODO результат работы(лог), true/false, error:<msg>, оценка //TODO результат работы(лог), true/false, error:<msg>, оценка

View File

@ -1,4 +1,4 @@
package ru.oa2.lti.domain.model.task; package ru.oa2.lti.application.dto.task;
public enum TaskType { public enum TaskType {
DOCKER, DOCKER,

View File

@ -1,9 +0,0 @@
package ru.oa2.lti.application.infrastructure.lms;
public interface LMSService {
String ltiAuth(String ltiLoginHint, String iss, String loginHint);
// Вызывается после LTI-запуска
String exchangeForAccessToken(String clientId);
}

View File

@ -1,12 +0,0 @@
package ru.oa2.lti.application.infrastructure.runner;
import java.util.UUID;
public interface Runner {
boolean run(UUID userId, String script);
boolean check(UUID userId, String script);
boolean delete(UUID userId, String script);
}

View File

@ -1,110 +0,0 @@
package ru.oa2.lti.application.infrastructure.runner;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import ru.oa2.lti.application.service.history.HistoryService;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.UUID;
@Slf4j
@Component
public class RunnerImpl implements Runner {
private final String TEMP_DIR = "./temp/";
private final HistoryService historyService;
public RunnerImpl(HistoryService historyService) {
this.historyService = historyService;
}
@Override
public boolean run(UUID userId, String script) {
try {
historyService.logAction(null, userId, "SCRIPT_RUN_START", "Init script execution started");
boolean result = runScript(userId, script);
if (result) {
historyService.logAction(null, userId, "SCRIPT_RUN_SUCCESS", "Init script completed successfully");
} else {
historyService.logAction(null, userId, "SCRIPT_RUN_FAILED", "Init script failed with non-zero exit code");
}
return result;
} catch (Exception ex) {
log.error(ex.getMessage());
historyService.logAction(null, userId, "SCRIPT_RUN_ERROR", "Init script error: " + ex.getMessage());
return false;
}
}
//TODO добавить вывод результата/интерпретации
@Override
public boolean check(UUID userId, String script) {
try {
historyService.logAction(null, userId, "SCRIPT_CHECK_START", "Verification script execution started");
boolean result = runScript(userId, script);
if (result) {
historyService.logAction(null, userId, "SCRIPT_CHECK_SUCCESS", "Verification script completed successfully");
} else {
historyService.logAction(null, userId, "SCRIPT_CHECK_FAILED", "Verification script failed");
}
return result;
} catch (Exception ex) {
log.error(ex.getMessage());
historyService.logAction(null, userId, "SCRIPT_CHECK_ERROR", "Verification script error: " + ex.getMessage());
return false;
}
}
@Override
public boolean delete(UUID userId, String script) {
//TODO реализовать метод удаления сущностей
historyService.logAction(null, userId, "SCRIPT_DELETE_CALLED", "Delete script called (not implemented)");
return false;
}
private boolean runScript(UUID userId, String script) throws Exception {
var processBuilder = new ProcessBuilder();
processBuilder.command("bash", saveFile(userId, script));
var process = processBuilder.start();
BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream())
);
StringBuilder output = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
output.append(line).append("\n");
}
log.info(output.toString());
int exitCode = process.waitFor();
return exitCode == 0;
}
private String saveFile(UUID userId, String script) throws IOException {
Path path = Paths.get(TEMP_DIR + userId.toString());
if (!Files.exists(path)) {
Files.createDirectories(path);
}
var tm = System.currentTimeMillis();
var scriptPath = Files.write(
Paths.get(TEMP_DIR + userId + "/" + tm),
script.getBytes(StandardCharsets.UTF_8));
return scriptPath.toString();
}
}

View File

@ -1,25 +0,0 @@
package ru.oa2.lti.application.service.history;
import java.util.UUID;
public interface HistoryService {
/**
* Логирование события
*
* @param deploymentId ID деплоймента LMS
* @param contentUuid UUID контента
* @param message Сообщение о событии
*/
void logEvent(UUID deploymentId, UUID contentUuid, String message);
/**
* Логирование события с автоматической меткой времени
*
* @param deploymentId ID деплоймента LMS
* @param contentUuid UUID контента
* @param action Действие (например, "LOGIN", "TASK_LOADED")
* @param details Детали события
*/
void logAction(UUID deploymentId, UUID contentUuid, String action, String details);
}

View File

@ -1,61 +0,0 @@
package ru.oa2.lti.application.service.history;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import ru.oa2.lti.infrastructure.adapter.persistence.jpa.repository.HistoryJpaRepository;
import ru.oa2.lti.infrastructure.adapter.persistence.jpa.entity.HistoryEntity;
import java.time.LocalDateTime;
import java.util.UUID;
@Slf4j
@Service
public class HistoryServiceImpl implements HistoryService {
private final HistoryJpaRepository historyRepository;
public HistoryServiceImpl(HistoryJpaRepository historyRepository) {
this.historyRepository = historyRepository;
}
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logEvent(UUID deploymentId, UUID contentUuid, String message) {
try {
HistoryEntity history = new HistoryEntity();
history.setDeploymentId(deploymentId);
history.setContentUuid(contentUuid);
history.setCreated(LocalDateTime.now());
history.setMessage(truncateMessage(message));
historyRepository.save(history);
log.debug("Event logged: deploymentId={}, contentUuid={}, message={}",
deploymentId, contentUuid, message);
} catch (Exception e) {
log.error("Failed to log event: deploymentId={}, contentUuid={}, message={}",
deploymentId, contentUuid, message, e);
}
}
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logAction(UUID deploymentId, UUID contentUuid, String action, String details) {
String message = String.format("[%s] %s", action, details);
logEvent(deploymentId, contentUuid, message);
}
/**
* Обрезка сообщения до максимальной длины (2048 символов)
*/
private String truncateMessage(String message) {
if (message == null) {
return "";
}
if (message.length() > 2048) {
return message.substring(0, 2045) + "...";
}
return message;
}
}

View File

@ -1,7 +1,7 @@
package ru.oa2.lti.application.service.jwt; package ru.oa2.lti.application.service.jwt;
import ru.oa2.lti.domain.model.auth.IdTokenPayload; import ru.oa2.lti.application.dto.auth.IdTokenPayload;
import ru.oa2.lti.domain.model.auth.Payload; import ru.oa2.lti.application.dto.auth.Payload;
public interface JwtService { public interface JwtService {

View File

@ -3,7 +3,7 @@ package ru.oa2.lti.application.service.jwt;
import com.nimbusds.jose.shaded.gson.internal.LinkedTreeMap; import com.nimbusds.jose.shaded.gson.internal.LinkedTreeMap;
import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import ru.oa2.lti.domain.model.auth.*; import ru.oa2.lti.application.dto.auth.*;
import java.time.Instant; import java.time.Instant;
import java.time.LocalDateTime; import java.time.LocalDateTime;

View File

@ -5,9 +5,9 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient; import org.springframework.web.client.RestClient;
import ru.oa2.lti.application.service.jwt.JwtService; import ru.oa2.lti.application.service.jwt.JwtService;
import ru.oa2.lti.domain.model.results.GradingType; import ru.oa2.lti.application.dto.result.GradingType;
import ru.oa2.lti.domain.model.results.ProgressType; import ru.oa2.lti.application.dto.result.ProgressType;
import ru.oa2.lti.domain.model.results.ResultRequest; import ru.oa2.lti.application.dto.result.ResultRequest;
import java.time.LocalDateTime; import java.time.LocalDateTime;

View File

@ -1,9 +1,9 @@
package ru.oa2.lti.application.service.task; package ru.oa2.lti.application.service.task;
import ru.oa2.lti.domain.model.ResultRequest; import ru.oa2.lti.application.dto.result.ResultResponse;
import ru.oa2.lti.domain.model.results.ResultResponse; import ru.oa2.lti.application.dto.task.CheckTaskRequest;
import ru.oa2.lti.domain.model.task.RequestUpdateTask; import ru.oa2.lti.application.dto.task.RequestUpdateTask;
import ru.oa2.lti.domain.model.task.TaskData; import ru.oa2.lti.application.dto.task.TaskData;
import java.util.UUID; import java.util.UUID;
@ -17,7 +17,7 @@ public interface TaskService {
/* /*
Запуск проверки через скрипт Запуск проверки через скрипт
*/ */
ResultResponse checkTask(ResultRequest resultRequest); ResultResponse checkTask(CheckTaskRequest request);
/* /*
Сохранение/обновление task-а Сохранение/обновление task-а

View File

@ -4,31 +4,29 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import ru.oa2.lti.application.infrastructure.lms.LMSService;
import ru.oa2.lti.application.infrastructure.runner.Runner;
import ru.oa2.lti.application.service.history.HistoryService;
import ru.oa2.lti.application.service.results.ResultService; import ru.oa2.lti.application.service.results.ResultService;
import ru.oa2.lti.domain.exception.TaskNotFoundException; import ru.oa2.lti.domain.exception.TaskNotFoundException;
import ru.oa2.lti.domain.model.Task; import ru.oa2.lti.domain.model.Task;
import ru.oa2.lti.domain.repository.TaskRepository; import ru.oa2.lti.domain.port.out.HistoryRepository;
import ru.oa2.lti.domain.port.out.LMSGateway;
import ru.oa2.lti.domain.port.out.ScriptExecutor;
import ru.oa2.lti.domain.port.out.TaskRepository;
import ru.oa2.lti.domain.valueobject.*; import ru.oa2.lti.domain.valueobject.*;
import ru.oa2.lti.domain.model.ResultRequest; import ru.oa2.lti.application.dto.result.ResultResponse;
import ru.oa2.lti.domain.model.results.ResultResponse; import ru.oa2.lti.application.dto.task.CheckTaskRequest;
import ru.oa2.lti.domain.model.task.RequestUpdateTask; import ru.oa2.lti.application.dto.task.RequestUpdateTask;
import ru.oa2.lti.domain.model.task.TaskData; import ru.oa2.lti.application.dto.task.TaskData;
import java.util.UUID; import java.util.UUID;
/** /**
* Реализация TaskService, следующая принципам Clean Architecture и DDD. * Реализация TaskService, следующая принципам Clean Architecture и DDD.
* *
* Улучшения по сравнению со старой версией: * Улучшения:
* - Использует доменную модель Task вместо JPA Entity * - Использует доменную модель Task вместо JPA Entity
* - Зависит от порта TaskRepository (domain), а не от конкретной реализации * - Зависит от портов (domain), а не от конкретных реализаций
* - Бизнес-логика теперь в доменной модели Task * - Бизнес-логика в доменной модели Task
* - Сервис только координирует взаимодействие между компонентами * - Сервис только координирует взаимодействие между компонентами
* - Явная обработка исключений
* - Транзакции только на методах записи
*/ */
@Slf4j @Slf4j
@Service @Service
@ -36,27 +34,24 @@ import java.util.UUID;
public class TaskServiceImpl implements TaskService { public class TaskServiceImpl implements TaskService {
private final TaskRepository taskRepository; private final TaskRepository taskRepository;
private final Runner runner; private final ScriptExecutor scriptExecutor;
private final HistoryService historyService; private final HistoryRepository historyRepository;
private final ResultService resultService; private final ResultService resultService;
private final LMSService lmsService; private final LMSGateway lmsGateway;
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public TaskData getTask(UUID contextId) { public TaskData getTask(UUID contextId) {
log.info("Getting task for context: {}", contextId); log.info("Getting task for context: {}", contextId);
// Используем доменный репозиторий через порт
Task task = taskRepository.findByContextId(contextId) Task task = taskRepository.findByContextId(contextId)
.orElseThrow(() -> new TaskNotFoundException(contextId)); .orElseThrow(() -> new TaskNotFoundException(contextId));
// Преобразуем в DTO для presentation layer
TaskData data = taskToDto(task); TaskData data = taskToDto(task);
// Выполняем инициализационный скрипт
Script initScript = task.getInitScript(); Script initScript = task.getInitScript();
if (initScript.isExecutable()) { if (initScript.isExecutable()) {
boolean success = runner.run(contextId, initScript.content()); boolean success = scriptExecutor.runInitScript(contextId, initScript.content());
if (!success) { if (!success) {
log.error("Init script failed for context: {}", contextId); log.error("Init script failed for context: {}", contextId);
throw new RuntimeException("Initialization script execution failed"); throw new RuntimeException("Initialization script execution failed");
@ -68,16 +63,12 @@ public class TaskServiceImpl implements TaskService {
@Override @Override
@Transactional @Transactional
public ResultResponse checkTask(ResultRequest resultRequest) { public ResultResponse checkTask(CheckTaskRequest request) {
log.info("Checking task for clientId: {}", resultRequest.clientId()); log.info("Checking task for clientId: {}", request.clientId());
// Получаем access token от LMS String accessToken = lmsGateway.exchangeForAccessToken(request.clientId());
String accessToken = lmsService.exchangeForAccessToken(resultRequest.clientId()); resultService.setResult(accessToken, request.idToken());
// Отправляем результат
resultService.setResult(accessToken, resultRequest.idToken());
// TODO: реализовать запуск скрипта проверки
return new ResultResponse("success"); return new ResultResponse("success");
} }
@ -87,21 +78,16 @@ public class TaskServiceImpl implements TaskService {
log.info("Saving task for context: {}", requestUpdateTask.getContextId()); log.info("Saving task for context: {}", requestUpdateTask.getContextId());
try { try {
// Загружаем существующую задачу
Task task = taskRepository.findByContextId(requestUpdateTask.getContextId()) Task task = taskRepository.findByContextId(requestUpdateTask.getContextId())
.orElseThrow(() -> new TaskNotFoundException(requestUpdateTask.getContextId())); .orElseThrow(() -> new TaskNotFoundException(requestUpdateTask.getContextId()));
String oldName = task.getName().value(); String oldName = task.getName().value();
// Обновляем задачу через доменную модель (бизнес-логика внутри)
updateTaskFromDto(task, requestUpdateTask.getData()); updateTaskFromDto(task, requestUpdateTask.getData());
// Сохраняем через порт репозитория
taskRepository.save(task); taskRepository.save(task);
// Логируем действие historyRepository.logAction(
historyService.logAction( null,
null, // deploymentId нужно получать из контекста
requestUpdateTask.getContextId(), requestUpdateTask.getContextId(),
"TASK_SAVED", "TASK_SAVED",
String.format("Task saved: %s (was: %s)", String.format("Task saved: %s (was: %s)",
@ -111,7 +97,7 @@ public class TaskServiceImpl implements TaskService {
return new ResultResponse("success"); return new ResultResponse("success");
} catch (TaskNotFoundException e) { } catch (TaskNotFoundException e) {
log.error("Task not found for context: {}", requestUpdateTask.getContextId(), e); log.error("Task not found for context: {}", requestUpdateTask.getContextId(), e);
historyService.logAction( historyRepository.logAction(
null, null,
requestUpdateTask.getContextId(), requestUpdateTask.getContextId(),
"TASK_SAVE_ERROR", "TASK_SAVE_ERROR",
@ -120,7 +106,7 @@ public class TaskServiceImpl implements TaskService {
throw e; throw e;
} catch (Exception e) { } catch (Exception e) {
log.error("Error saving task", e); log.error("Error saving task", e);
historyService.logAction( historyRepository.logAction(
null, null,
requestUpdateTask.getContextId(), requestUpdateTask.getContextId(),
"TASK_SAVE_ERROR", "TASK_SAVE_ERROR",
@ -130,9 +116,6 @@ public class TaskServiceImpl implements TaskService {
} }
} }
/**
* Преобразует доменную модель Task в DTO TaskData
*/
private TaskData taskToDto(Task task) { private TaskData taskToDto(Task task) {
return new TaskData( return new TaskData(
task.getName().value(), task.getName().value(),
@ -143,17 +126,12 @@ public class TaskServiceImpl implements TaskService {
); );
} }
/**
* Обновляет доменную модель Task из DTO TaskData
*/
private void updateTaskFromDto(Task task, TaskData data) { private void updateTaskFromDto(Task task, TaskData data) {
// Используем Value Objects с валидацией
task.update( task.update(
TaskName.of(data.getName()), TaskName.of(data.getName()),
Description.of(data.getDescription()) Description.of(data.getDescription())
); );
// Обновляем скрипты
if (data.getInitScript() != null) { if (data.getInitScript() != null) {
task.setScript(Script.of(ScriptType.INITIALIZATION, data.getInitScript())); task.setScript(Script.of(ScriptType.INITIALIZATION, data.getInitScript()));
} }

View File

@ -0,0 +1,48 @@
package ru.oa2.lti.application.usecase.lti;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import ru.oa2.lti.application.dto.auth.IdTokenPayload;
import ru.oa2.lti.application.service.jwt.JwtService;
import ru.oa2.lti.domain.port.out.HistoryRepository;
/**
* Use Case для обработки редиректа после LTI аутентификации.
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class HandleLtiRedirectUseCase {
private final JwtService jwtService;
private final HistoryRepository historyRepository;
/**
* Результат обработки редиректа
*/
public record LtiRedirectResult(
IdTokenPayload idTokenPayload,
String redirectUrl
) {}
/**
* Обрабатывает редирект после OAuth аутентификации
*/
public LtiRedirectResult execute(String idToken, String state) {
log.info("Handling LTI redirect");
// Декодируем id_token
var idTokenPayload = jwtService.getTokenPayload(idToken);
// Логируем перенаправление
historyRepository.logAction(
idTokenPayload.getDeploymentId(),
idTokenPayload.getContext() != null ? idTokenPayload.getContext().id() : null,
"LTI_REDIRECT",
"User redirected after OAuth authentication"
);
return new LtiRedirectResult(idTokenPayload, "/tool/lti/task");
}
}

View File

@ -0,0 +1,88 @@
package ru.oa2.lti.application.usecase.lti;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import ru.oa2.lti.application.dto.auth.LtiLogin;
import ru.oa2.lti.application.dto.auth.Payload;
import ru.oa2.lti.application.service.jwt.JwtService;
import ru.oa2.lti.domain.port.out.HistoryRepository;
import ru.oa2.lti.domain.port.out.LMSGateway;
/**
* Use Case для обработки LTI логина.
* Инкапсулирует бизнес-логику аутентификации через LTI 1.3.
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class ProcessLtiLoginUseCase {
private final JwtService jwtService;
private final LMSGateway lmsGateway;
private final HistoryRepository historyRepository;
/**
* Результат обработки LTI логина
*/
public record LtiLoginResult(
LtiLogin ltiLogin,
Payload payload,
String redirectResponse,
boolean requiresSelection
) {}
/**
* Команда для обработки LTI логина
*/
public record LtiLoginCommand(
String clientId,
String iss,
String loginHint,
String ltiDeploymentId,
String ltiMessageHint,
String targetLinkUri
) {}
/**
* Обрабатывает LTI логин и возвращает результат
*/
public LtiLoginResult execute(LtiLoginCommand command) {
log.info("Processing LTI login for clientId: {}", command.clientId());
// Создаём объект LtiLogin
var ltiLogin = LtiLogin.builder()
.clientId(command.clientId())
.iss(command.iss())
.loginHint(command.loginHint())
.ltiDeploymentId(command.ltiDeploymentId())
.ltiMessageHint(command.ltiMessageHint())
.targetLinkUri(command.targetLinkUri())
.build();
// Получаем payload из JWT
var payload = jwtService.getPayload(ltiLogin.getLoginHint());
// Логируем событие входа
historyRepository.logAction(
payload.getDeploymentId(),
payload.getContextId(),
"LTI_LOGIN",
String.format("User login via LTI 1.3, clientId=%s", command.clientId())
);
// Проверяем, есть ли contextId
if (payload.getContextId() != null) {
// Выполняем аутентификацию в LMS
String redirectResponse = lmsGateway.ltiAuth(
command.ltiMessageHint(),
command.iss(),
command.loginHint()
);
return new LtiLoginResult(ltiLogin, payload, redirectResponse, false);
}
// Требуется выбор задания
return new LtiLoginResult(ltiLogin, payload, null, true);
}
}

View File

@ -1,8 +0,0 @@
package ru.oa2.lti.domain.model;
//TODO 2 одинаковых имени? )
public record ResultRequest(
String clientId,
String idToken
) {
}

View File

@ -0,0 +1,31 @@
package ru.oa2.lti.domain.port.out;
import java.util.UUID;
/**
* Выходной порт для записи истории событий.
* Определяет контракт для логирования действий в системе.
*
* Принципы:
* - Hexagonal Architecture: это выходной порт (driven port)
* - Domain слой определяет интерфейс, Infrastructure реализует
*/
public interface HistoryRepository {
/**
* Записывает событие в историю
* @param deploymentId идентификатор развертывания
* @param contentUuid UUID контента
* @param message сообщение события
*/
void logEvent(UUID deploymentId, UUID contentUuid, String message);
/**
* Записывает действие с типом в историю
* @param deploymentId идентификатор развертывания
* @param contentUuid UUID контента
* @param actionType тип действия
* @param details детали действия
*/
void logAction(UUID deploymentId, UUID contentUuid, String actionType, String details);
}

View File

@ -0,0 +1,28 @@
package ru.oa2.lti.domain.port.out;
/**
* Выходной порт для интеграции с LMS (Learning Management System).
* Определяет контракт для взаимодействия с внешней LMS системой.
*
* Принципы:
* - Hexagonal Architecture: это выходной порт (driven port)
* - Domain слой определяет интерфейс, Infrastructure реализует
*/
public interface LMSGateway {
/**
* Выполняет LTI аутентификацию
* @param ltiLoginHint подсказка для входа LTI
* @param iss издатель токена
* @param loginHint подсказка для входа
* @return URL для редиректа или HTML ответ
*/
String ltiAuth(String ltiLoginHint, String iss, String loginHint);
/**
* Обменивает учетные данные на access token для работы с LMS API
* @param clientId идентификатор клиента
* @return access token
*/
String exchangeForAccessToken(String clientId);
}

View File

@ -0,0 +1,38 @@
package ru.oa2.lti.domain.port.out;
import java.util.UUID;
/**
* Выходной порт для выполнения скриптов.
* Определяет контракт для запуска, проверки и удаления скриптов.
*
* Принципы:
* - Hexagonal Architecture: это выходной порт (driven port)
* - Domain слой определяет интерфейс, Infrastructure реализует
*/
public interface ScriptExecutor {
/**
* Запускает инициализационный скрипт для пользователя
* @param contextId идентификатор контекста
* @param script содержимое скрипта
* @return true если скрипт выполнен успешно
*/
boolean runInitScript(UUID contextId, String script);
/**
* Запускает скрипт проверки для пользователя
* @param contextId идентификатор контекста
* @param script содержимое скрипта
* @return true если проверка прошла успешно
*/
boolean runVerificationScript(UUID contextId, String script);
/**
* Запускает скрипт удаления/очистки для пользователя
* @param contextId идентификатор контекста
* @param script содержимое скрипта
* @return true если очистка прошла успешно
*/
boolean runCleanupScript(UUID contextId, String script);
}

View File

@ -1,4 +1,4 @@
package ru.oa2.lti.domain.repository; package ru.oa2.lti.domain.port.out;
import ru.oa2.lti.domain.model.Task; import ru.oa2.lti.domain.model.Task;
import ru.oa2.lti.domain.valueobject.TaskId; import ru.oa2.lti.domain.valueobject.TaskId;

View File

@ -0,0 +1,81 @@
package ru.oa2.lti.infrastructure.adapter.history;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import ru.oa2.lti.domain.port.out.HistoryRepository;
import ru.oa2.lti.infrastructure.adapter.persistence.jpa.entity.HistoryEntity;
import ru.oa2.lti.infrastructure.adapter.persistence.jpa.repository.HistoryJpaRepository;
import java.time.LocalDateTime;
import java.util.UUID;
/**
* JPA адаптер для записи истории событий.
* Реализует выходной порт HistoryRepository.
*
* Принципы:
* - Hexagonal Architecture: это исходящий адаптер (driven adapter)
* - Инкапсулирует детали работы с JPA
*/
@Slf4j
@Component
public class HistoryJpaAdapter implements HistoryRepository {
private static final int MAX_MESSAGE_LENGTH = 2048;
private final HistoryJpaRepository jpaRepository;
public HistoryJpaAdapter(HistoryJpaRepository jpaRepository) {
this.jpaRepository = jpaRepository;
}
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logEvent(UUID deploymentId, UUID contentUuid, String message) {
try {
HistoryEntity history = new HistoryEntity();
history.setDeploymentId(deploymentId);
history.setContentUuid(contentUuid);
history.setCreated(LocalDateTime.now());
history.setMessage(truncateMessage(message));
jpaRepository.save(history);
log.debug("Event logged: deploymentId={}, contentUuid={}, message={}",
deploymentId, contentUuid, message);
} catch (Exception e) {
log.error("Failed to log event: deploymentId={}, contentUuid={}, message={}",
deploymentId, contentUuid, message, e);
}
}
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logAction(UUID deploymentId, UUID contentUuid, String actionType, String details) {
try {
HistoryEntity history = new HistoryEntity();
history.setDeploymentId(deploymentId);
history.setContentUuid(contentUuid);
history.setCreated(LocalDateTime.now());
history.setActionType(actionType);
history.setMessage(truncateMessage(details));
jpaRepository.save(history);
log.debug("Action logged: deploymentId={}, contentUuid={}, action={}, details={}",
deploymentId, contentUuid, actionType, details);
} catch (Exception e) {
log.error("Failed to log action: deploymentId={}, contentUuid={}, action={}",
deploymentId, contentUuid, actionType, e);
}
}
private String truncateMessage(String message) {
if (message == null) {
return "";
}
if (message.length() > MAX_MESSAGE_LENGTH) {
return message.substring(0, MAX_MESSAGE_LENGTH - 3) + "...";
}
return message;
}
}

View File

@ -1,4 +1,4 @@
package ru.oa2.lti.application.infrastructure.lms; package ru.oa2.lti.infrastructure.adapter.lms;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
@ -8,19 +8,27 @@ import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestClient; import org.springframework.web.client.RestClient;
import ru.oa2.lti.application.config.AppProperties; import ru.oa2.lti.application.config.AppProperties;
import ru.oa2.lti.application.service.jwt.JwtAssertionGenerator; import ru.oa2.lti.application.service.jwt.JwtAssertionGenerator;
import ru.oa2.lti.domain.model.auth.Scope; import ru.oa2.lti.application.dto.auth.Scope;
import ru.oa2.lti.domain.port.out.LMSGateway;
import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
/**
* Адаптер для интеграции с LMS через REST API.
* Реализует выходной порт LMSGateway.
*
* Принципы:
* - Hexagonal Architecture: это исходящий адаптер (driven adapter)
* - Инкапсулирует детали HTTP взаимодействия с LMS
*/
@Slf4j @Slf4j
@Component @Component
public class LMSServiceImpl implements LMSService { public class LMSRestAdapter implements LMSGateway {
private final RestClient restClient; private final RestClient restClient;
private final AppProperties appProperties; private final AppProperties appProperties;
public LMSServiceImpl(RestClient restClient, AppProperties appProperties) { public LMSRestAdapter(RestClient restClient, AppProperties appProperties) {
this.restClient = restClient; this.restClient = restClient;
this.appProperties = appProperties; this.appProperties = appProperties;
} }
@ -30,8 +38,8 @@ public class LMSServiceImpl implements LMSService {
var result = restClient var result = restClient
.get() .get()
.uri("/lti/auth?" + .uri("/lti/auth?" +
"state=start" + //TODO а какие могут быть статусы? нужно в Enum перенести "state=start" +
"&code=code1" + //TODO что за коды? "&code=code1" +
"&iss=" + iss + "&iss=" + iss +
"&login_hint=" + loginHint + "&login_hint=" + loginHint +
"&redirect_uri=" + appProperties.lmsUrl() + "/tool/lti/redirect" + "&redirect_uri=" + appProperties.lmsUrl() + "/tool/lti/redirect" +
@ -47,11 +55,9 @@ public class LMSServiceImpl implements LMSService {
@Override @Override
public String exchangeForAccessToken(String clientId) { public String exchangeForAccessToken(String clientId) {
var endpointUrl = appProperties.lmsUrl() + "/lti/token"; var endpointUrl = appProperties.lmsUrl() + "/lti/token";
try { try {
var clientAssertion = getClientAssertion(clientId, endpointUrl); var clientAssertion = getClientAssertion(clientId, endpointUrl);
var body = getBody(clientAssertion); var body = getBody(clientAssertion);
@ -66,34 +72,31 @@ public class LMSServiceImpl implements LMSService {
if (responseBody != null && responseBody.containsKey("access_token")) { if (responseBody != null && responseBody.containsKey("access_token")) {
return (String) responseBody.get("access_token"); return (String) responseBody.get("access_token");
} else { } else {
throw new RuntimeException("Ответ не содержит access_token"); throw new RuntimeException("Response does not contain access_token");
} }
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException("Ошибка получения токена: " + e.getMessage(), e); throw new RuntimeException("Error getting token: " + e.getMessage(), e);
} }
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException("Не удалось получить access token", e); throw new RuntimeException("Failed to get access token", e);
} }
} }
private String getClientAssertion(String clientId, String endpointUrl) throws Exception { private String getClientAssertion(String clientId, String endpointUrl) throws Exception {
// Генерируем client_assertion
return JwtAssertionGenerator.generateClientAssertion( return JwtAssertionGenerator.generateClientAssertion(
clientId, clientId,
endpointUrl, endpointUrl,
"my-key-id-1" //TODO должен совпадать с тем, что в JWKS "my-key-id-1"
); );
} }
private MultiValueMap<String, String> getBody(String clientAssertion) { private MultiValueMap<String, String> getBody(String clientAssertion) {
MultiValueMap<String, String> body = new LinkedMultiValueMap<>(); MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("grant_type", "client_credentials"); body.add("grant_type", "client_credentials");
body.add("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"); body.add("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer");
body.add("client_assertion", clientAssertion); body.add("client_assertion", clientAssertion);
body.add("scope", Scope.scope(Scope.LINE_ITEM, Scope.SCOPE, Scope.ENDPOINT)); body.add("scope", Scope.scope(Scope.LINE_ITEM, Scope.SCOPE, Scope.ENDPOINT));
return body; return body;
} }
} }

View File

@ -3,7 +3,7 @@ package ru.oa2.lti.infrastructure.adapter.persistence.jpa;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import ru.oa2.lti.domain.model.Task; import ru.oa2.lti.domain.model.Task;
import ru.oa2.lti.domain.repository.TaskRepository; import ru.oa2.lti.domain.port.out.TaskRepository;
import ru.oa2.lti.domain.valueobject.TaskId; import ru.oa2.lti.domain.valueobject.TaskId;
import ru.oa2.lti.infrastructure.adapter.persistence.jpa.entity.TaskEntity; import ru.oa2.lti.infrastructure.adapter.persistence.jpa.entity.TaskEntity;
import ru.oa2.lti.infrastructure.adapter.persistence.jpa.mapper.TaskEntityMapper; import ru.oa2.lti.infrastructure.adapter.persistence.jpa.mapper.TaskEntityMapper;

View File

@ -0,0 +1,130 @@
package ru.oa2.lti.infrastructure.adapter.script;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import ru.oa2.lti.domain.port.out.HistoryRepository;
import ru.oa2.lti.domain.port.out.ScriptExecutor;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.UUID;
/**
* Адаптер для выполнения Bash скриптов.
* Реализует выходной порт ScriptExecutor.
*
* Принципы:
* - Hexagonal Architecture: это исходящий адаптер (driven adapter)
* - Инкапсулирует детали выполнения скриптов через bash
*/
@Slf4j
@Component
public class BashScriptAdapter implements ScriptExecutor {
private static final String TEMP_DIR = "./temp/";
private final HistoryRepository historyRepository;
public BashScriptAdapter(HistoryRepository historyRepository) {
this.historyRepository = historyRepository;
}
@Override
public boolean runInitScript(UUID contextId, String script) {
try {
historyRepository.logAction(null, contextId, "SCRIPT_RUN_START", "Init script execution started");
boolean result = runScript(contextId, script);
if (result) {
historyRepository.logAction(null, contextId, "SCRIPT_RUN_SUCCESS", "Init script completed successfully");
} else {
historyRepository.logAction(null, contextId, "SCRIPT_RUN_FAILED", "Init script failed with non-zero exit code");
}
return result;
} catch (Exception ex) {
log.error("Init script error: {}", ex.getMessage());
historyRepository.logAction(null, contextId, "SCRIPT_RUN_ERROR", "Init script error: " + ex.getMessage());
return false;
}
}
@Override
public boolean runVerificationScript(UUID contextId, String script) {
try {
historyRepository.logAction(null, contextId, "SCRIPT_CHECK_START", "Verification script execution started");
boolean result = runScript(contextId, script);
if (result) {
historyRepository.logAction(null, contextId, "SCRIPT_CHECK_SUCCESS", "Verification script completed successfully");
} else {
historyRepository.logAction(null, contextId, "SCRIPT_CHECK_FAILED", "Verification script failed");
}
return result;
} catch (Exception ex) {
log.error("Verification script error: {}", ex.getMessage());
historyRepository.logAction(null, contextId, "SCRIPT_CHECK_ERROR", "Verification script error: " + ex.getMessage());
return false;
}
}
@Override
public boolean runCleanupScript(UUID contextId, String script) {
try {
historyRepository.logAction(null, contextId, "SCRIPT_DELETE_START", "Cleanup script execution started");
boolean result = runScript(contextId, script);
if (result) {
historyRepository.logAction(null, contextId, "SCRIPT_DELETE_SUCCESS", "Cleanup script completed successfully");
} else {
historyRepository.logAction(null, contextId, "SCRIPT_DELETE_FAILED", "Cleanup script failed");
}
return result;
} catch (Exception ex) {
log.error("Cleanup script error: {}", ex.getMessage());
historyRepository.logAction(null, contextId, "SCRIPT_DELETE_ERROR", "Cleanup script error: " + ex.getMessage());
return false;
}
}
private boolean runScript(UUID contextId, String script) throws Exception {
var processBuilder = new ProcessBuilder();
processBuilder.command("bash", saveFile(contextId, script));
var process = processBuilder.start();
BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream())
);
StringBuilder output = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
output.append(line).append("\n");
}
log.info("Script output: {}", output);
int exitCode = process.waitFor();
return exitCode == 0;
}
private String saveFile(UUID contextId, String script) throws IOException {
Path path = Paths.get(TEMP_DIR + contextId.toString());
if (!Files.exists(path)) {
Files.createDirectories(path);
}
var tm = System.currentTimeMillis();
var scriptPath = Files.write(
Paths.get(TEMP_DIR + contextId + "/" + tm),
script.getBytes(StandardCharsets.UTF_8));
return scriptPath.toString();
}
}