diff --git a/pom.xml b/pom.xml index b6902cb..e1aa7a0 100644 --- a/pom.xml +++ b/pom.xml @@ -19,6 +19,7 @@ 21 + 1.6.3 @@ -48,6 +49,12 @@ lombok + + org.mapstruct + mapstruct + ${mapstruct.version} + + org.springframework.boot spring-boot-starter-test @@ -87,6 +94,33 @@ + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + ${java.version} + ${java.version} + + + org.projectlombok + lombok + ${lombok.version} + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + + + org.projectlombok + lombok-mapstruct-binding + 0.2.0 + + + + + diff --git a/src/main/java/ru/oa2/lti/application/infrastructure/repository/HistoryRepository.java b/src/main/java/ru/oa2/lti/application/infrastructure/repository/HistoryRepository.java index de4254b..ae7edeb 100644 --- a/src/main/java/ru/oa2/lti/application/infrastructure/repository/HistoryRepository.java +++ b/src/main/java/ru/oa2/lti/application/infrastructure/repository/HistoryRepository.java @@ -2,15 +2,20 @@ package ru.oa2.lti.application.infrastructure.repository; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; -import ru.oa2.lti.application.infrastructure.repository.entities.History; +import ru.oa2.lti.infrastructure.adapter.persistence.jpa.entity.HistoryEntity; import java.util.List; import java.util.UUID; +/** + * @deprecated Используйте {@link ru.oa2.lti.infrastructure.adapter.persistence.jpa.repository.HistoryJpaRepository} + * Этот интерфейс оставлен для обратной совместимости и будет удален в следующей версии. + */ +@Deprecated @Repository -public interface HistoryRepository extends JpaRepository { +public interface HistoryRepository extends JpaRepository { - List findByContentUuidOrderByCreatedDesc(UUID contentUuid); + List findByContentUuidOrderByCreatedDesc(UUID contentUuid); - List findByDeploymentIdOrderByCreatedDesc(UUID deploymentId); + List findByDeploymentIdOrderByCreatedDesc(UUID deploymentId); } diff --git a/src/main/java/ru/oa2/lti/application/infrastructure/repository/LMSContentRepository.java b/src/main/java/ru/oa2/lti/application/infrastructure/repository/LMSContentRepository.java index f8f70cd..24c46fe 100644 --- a/src/main/java/ru/oa2/lti/application/infrastructure/repository/LMSContentRepository.java +++ b/src/main/java/ru/oa2/lti/application/infrastructure/repository/LMSContentRepository.java @@ -2,13 +2,18 @@ package ru.oa2.lti.application.infrastructure.repository; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; -import ru.oa2.lti.application.infrastructure.repository.entities.LMSContent; +import ru.oa2.lti.infrastructure.adapter.persistence.jpa.entity.LMSContentEntity; import java.util.Optional; import java.util.UUID; +/** + * @deprecated Используйте {@link ru.oa2.lti.infrastructure.adapter.persistence.jpa.repository.LMSContentJpaRepository} + * Этот интерфейс оставлен для обратной совместимости и будет удален в следующей версии. + */ +@Deprecated @Repository -public interface LMSContentRepository extends JpaRepository { +public interface LMSContentRepository extends JpaRepository { - Optional getLMSContentByContentUuid(UUID contentUuid); + Optional getLMSContentByContentUuid(UUID contextId); } diff --git a/src/main/java/ru/oa2/lti/application/infrastructure/repository/entities/History.java b/src/main/java/ru/oa2/lti/application/infrastructure/repository/entities/History.java index 8a0fe48..44ab5d0 100644 --- a/src/main/java/ru/oa2/lti/application/infrastructure/repository/entities/History.java +++ b/src/main/java/ru/oa2/lti/application/infrastructure/repository/entities/History.java @@ -7,6 +7,11 @@ import lombok.Setter; import java.time.LocalDateTime; import java.util.UUID; +/** + * @deprecated Используйте {@link ru.oa2.lti.infrastructure.adapter.persistence.jpa.entity.HistoryEntity} + * Этот класс оставлен для обратной совместимости и будет удален в следующей версии. + */ +@Deprecated @Getter @Setter @Entity diff --git a/src/main/java/ru/oa2/lti/application/infrastructure/repository/entities/LMSContent.java b/src/main/java/ru/oa2/lti/application/infrastructure/repository/entities/LMSContent.java index ff9f663..358d3be 100644 --- a/src/main/java/ru/oa2/lti/application/infrastructure/repository/entities/LMSContent.java +++ b/src/main/java/ru/oa2/lti/application/infrastructure/repository/entities/LMSContent.java @@ -7,6 +7,11 @@ import lombok.Setter; import java.time.LocalDateTime; import java.util.UUID; +/** + * @deprecated Используйте {@link ru.oa2.lti.infrastructure.adapter.persistence.jpa.entity.LMSContentEntity} + * Этот класс оставлен для обратной совместимости и будет удален в следующей версии. + */ +@Deprecated @Getter @Setter @Entity @@ -29,7 +34,6 @@ public class LMSContent { @Column(name = "created", nullable = false) LocalDateTime created; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "task_id", nullable = false, foreignKey = @ForeignKey(name = "fk_lms_content_task")) - Task task; + @Column(name = "task_id", nullable = false) + Long taskId; } diff --git a/src/main/java/ru/oa2/lti/application/infrastructure/repository/entities/Task.java b/src/main/java/ru/oa2/lti/application/infrastructure/repository/entities/Task.java index 0920297..3db8145 100644 --- a/src/main/java/ru/oa2/lti/application/infrastructure/repository/entities/Task.java +++ b/src/main/java/ru/oa2/lti/application/infrastructure/repository/entities/Task.java @@ -4,6 +4,11 @@ import jakarta.persistence.*; import lombok.Getter; import lombok.Setter; +/** + * @deprecated Используйте {@link ru.oa2.lti.infrastructure.adapter.persistence.jpa.entity.TaskEntity} + * Этот класс оставлен для обратной совместимости и будет удален в следующей версии. + */ +@Deprecated @Getter @Setter @Entity diff --git a/src/main/java/ru/oa2/lti/application/service/history/HistoryServiceImpl.java b/src/main/java/ru/oa2/lti/application/service/history/HistoryServiceImpl.java index d0e32cc..f57d706 100644 --- a/src/main/java/ru/oa2/lti/application/service/history/HistoryServiceImpl.java +++ b/src/main/java/ru/oa2/lti/application/service/history/HistoryServiceImpl.java @@ -5,7 +5,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import ru.oa2.lti.application.infrastructure.repository.HistoryRepository; -import ru.oa2.lti.application.infrastructure.repository.entities.History; +import ru.oa2.lti.infrastructure.adapter.persistence.jpa.entity.HistoryEntity; import java.time.LocalDateTime; import java.util.UUID; @@ -24,7 +24,7 @@ public class HistoryServiceImpl implements HistoryService { @Transactional(propagation = Propagation.REQUIRES_NEW) public void logEvent(UUID deploymentId, UUID contentUuid, String message) { try { - History history = new History(); + HistoryEntity history = new HistoryEntity(); history.setDeploymentId(deploymentId); history.setContentUuid(contentUuid); history.setCreated(LocalDateTime.now()); diff --git a/src/main/java/ru/oa2/lti/application/service/task/TaskServiceImpl.java b/src/main/java/ru/oa2/lti/application/service/task/TaskServiceImpl.java index 4c2e8e8..2170f85 100644 --- a/src/main/java/ru/oa2/lti/application/service/task/TaskServiceImpl.java +++ b/src/main/java/ru/oa2/lti/application/service/task/TaskServiceImpl.java @@ -1,15 +1,17 @@ package ru.oa2.lti.application.service.task; -import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import ru.oa2.lti.application.infrastructure.lms.LMSService; -import ru.oa2.lti.application.infrastructure.repository.LMSContentRepository; -import ru.oa2.lti.application.service.history.HistoryService; -import ru.oa2.lti.application.infrastructure.repository.entities.LMSContent; -import ru.oa2.lti.application.infrastructure.repository.entities.Task; 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.domain.exception.TaskNotFoundException; +import ru.oa2.lti.domain.model.Task; +import ru.oa2.lti.domain.repository.TaskRepository; +import ru.oa2.lti.domain.valueobject.*; import ru.oa2.lti.domain.model.ResultRequest; import ru.oa2.lti.domain.model.results.ResultResponse; import ru.oa2.lti.domain.model.task.RequestUpdateTask; @@ -17,128 +19,149 @@ import ru.oa2.lti.domain.model.task.TaskData; import java.util.UUID; +/** + * Реализация TaskService, следующая принципам Clean Architecture и DDD. + * + * Улучшения по сравнению со старой версией: + * - Использует доменную модель Task вместо JPA Entity + * - Зависит от порта TaskRepository (domain), а не от конкретной реализации + * - Бизнес-логика теперь в доменной модели Task + * - Сервис только координирует взаимодействие между компонентами + * - Явная обработка исключений + * - Транзакции только на методах записи + */ @Slf4j @Service -@Transactional(rollbackOn = Throwable.class) +@RequiredArgsConstructor public class TaskServiceImpl implements TaskService { - private final LMSContentRepository lmsContentRepository; + private final TaskRepository taskRepository; private final Runner runner; private final HistoryService historyService; - - //TODO объединить в один? - final ResultService resultService; - final LMSService lmsService; - - public TaskServiceImpl(LMSContentRepository lmsContentRepository, - Runner runner, - ResultService resultService, - LMSService lmsService, - HistoryService historyService) { - - this.lmsContentRepository = lmsContentRepository; - this.runner = runner; - this.resultService = resultService; - this.lmsService = lmsService; - this.historyService = historyService; - } + private final ResultService resultService; + private final LMSService lmsService; @Override + @Transactional(readOnly = true) public TaskData getTask(UUID contextId) { - var content = lmsContentRepository.getLMSContentByContentUuid(contextId); + log.info("Getting task for context: {}", contextId); - if(content.isPresent()) { - LMSContent lmsContent = content.get(); - Task task = lmsContent.getTask(); + // Используем доменный репозиторий через порт + Task task = taskRepository.findByContextId(contextId) + .orElseThrow(() -> new TaskNotFoundException(contextId)); - if (task == null) { - return new TaskData("Not Page", "", "", "", ""); + // Преобразуем в DTO для presentation layer + TaskData data = taskToDto(task); + + // Выполняем инициализационный скрипт + Script initScript = task.getInitScript(); + if (initScript.isExecutable()) { + boolean success = runner.run(contextId, initScript.content()); + if (!success) { + log.error("Init script failed for context: {}", contextId); + throw new RuntimeException("Initialization script execution failed"); } - - TaskData data = entityToTaskData(task); - - //TODO string -> exception? - if (!runner.run(contextId, data.getInitScript())) { - return new TaskData("Init script FAILED", "", "", "", ""); - } - - return data; - } else { - return new TaskData("Not Page", "", "", "", ""); } + + return data; } @Override + @Transactional public ResultResponse checkTask(ResultRequest resultRequest) { + log.info("Checking task for clientId: {}", resultRequest.clientId()); - //TODO запуск скрипта проверки - var assessToken = lmsService.exchangeForAccessToken( - resultRequest.clientId()); - resultService.setResult(assessToken, resultRequest.idToken()); + // Получаем access token от LMS + String accessToken = lmsService.exchangeForAccessToken(resultRequest.clientId()); - //TODO обработка и запуск скрипта + // Отправляем результат + resultService.setResult(accessToken, resultRequest.idToken()); + + // TODO: реализовать запуск скрипта проверки return new ResultResponse("success"); } @Override + @Transactional public ResultResponse saveTask(RequestUpdateTask requestUpdateTask) { + log.info("Saving task for context: {}", requestUpdateTask.getContextId()); - log.info("save"); - var result = lmsContentRepository.getLMSContentByContentUuid(requestUpdateTask.getContextId()); + try { + // Загружаем существующую задачу + Task task = taskRepository.findByContextId(requestUpdateTask.getContextId()) + .orElseThrow(() -> new TaskNotFoundException(requestUpdateTask.getContextId())); - //TODO доработать версионирование task-ов - if (result.isPresent()) { - LMSContent content = result.get(); - Task task = content.getTask(); + String oldName = task.getName().value(); - if (task != null) { - String oldName = task.getName(); - updateTaskFromData(task, requestUpdateTask.getData()); - lmsContentRepository.save(content); + // Обновляем задачу через доменную модель (бизнес-логика внутри) + updateTaskFromDto(task, requestUpdateTask.getData()); - historyService.logAction( - content.getDeploymentId(), - requestUpdateTask.getContextId(), - "TASK_SAVED", - String.format("Task saved: %s (was: %s)", - requestUpdateTask.getData().getName(), oldName) - ); - } else { - historyService.logAction( - content.getDeploymentId(), - requestUpdateTask.getContextId(), - "TASK_SAVE_ERROR", - "Task is null, cannot save" - ); - } - } else { + // Сохраняем через порт репозитория + taskRepository.save(task); + + // Логируем действие + historyService.logAction( + null, // deploymentId нужно получать из контекста + requestUpdateTask.getContextId(), + "TASK_SAVED", + String.format("Task saved: %s (was: %s)", + requestUpdateTask.getData().getName(), oldName) + ); + + return new ResultResponse("success"); + } catch (TaskNotFoundException e) { + log.error("Task not found for context: {}", requestUpdateTask.getContextId(), e); historyService.logAction( null, requestUpdateTask.getContextId(), "TASK_SAVE_ERROR", - "Content not found" + "Task not found: " + e.getMessage() ); + throw e; + } catch (Exception e) { + log.error("Error saving task", e); + historyService.logAction( + null, + requestUpdateTask.getContextId(), + "TASK_SAVE_ERROR", + "Error: " + e.getMessage() + ); + throw new RuntimeException("Failed to save task", e); } - - //TODO обработка exception - failed / success - return new ResultResponse("success"); } - private TaskData entityToTaskData(Task task) { + /** + * Преобразует доменную модель Task в DTO TaskData + */ + private TaskData taskToDto(Task task) { return new TaskData( - task.getName(), - task.getDescription(), - task.getInitScript(), - task.getVerificationScript(), - task.getDeleteScript() + task.getName().value(), + task.getDescription().value(), + task.getInitScript().content(), + task.getVerificationScript().content(), + task.getDeleteScript().content() ); } - private void updateTaskFromData(Task task, TaskData data) { - task.setName(data.getName()); - task.setDescription(data.getDescription()); - task.setInitScript(data.getInitScript()); - task.setVerificationScript(data.getVerificationScript()); - task.setDeleteScript(data.getDeleteScript()); + /** + * Обновляет доменную модель Task из DTO TaskData + */ + private void updateTaskFromDto(Task task, TaskData data) { + // Используем Value Objects с валидацией + task.update( + TaskName.of(data.getName()), + Description.of(data.getDescription()) + ); + + // Обновляем скрипты + if (data.getInitScript() != null) { + task.setScript(Script.of(ScriptType.INITIALIZATION, data.getInitScript())); + } + if (data.getVerificationScript() != null) { + task.setScript(Script.of(ScriptType.VERIFICATION, data.getVerificationScript())); + } + if (data.getDeleteScript() != null) { + task.setScript(Script.of(ScriptType.DELETION, data.getDeleteScript())); + } } } diff --git a/src/main/java/ru/oa2/lti/domain/exception/DomainException.java b/src/main/java/ru/oa2/lti/domain/exception/DomainException.java new file mode 100644 index 0000000..c844efe --- /dev/null +++ b/src/main/java/ru/oa2/lti/domain/exception/DomainException.java @@ -0,0 +1,14 @@ +package ru.oa2.lti.domain.exception; + +/** + * Базовое исключение для доменного слоя + */ +public abstract class DomainException extends RuntimeException { + protected DomainException(String message) { + super(message); + } + + protected DomainException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/ru/oa2/lti/domain/exception/InvalidTaskStateException.java b/src/main/java/ru/oa2/lti/domain/exception/InvalidTaskStateException.java new file mode 100644 index 0000000..1de5eef --- /dev/null +++ b/src/main/java/ru/oa2/lti/domain/exception/InvalidTaskStateException.java @@ -0,0 +1,10 @@ +package ru.oa2.lti.domain.exception; + +/** + * Исключение при невалидном состоянии задачи + */ +public class InvalidTaskStateException extends DomainException { + public InvalidTaskStateException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/oa2/lti/domain/exception/TaskNotFoundException.java b/src/main/java/ru/oa2/lti/domain/exception/TaskNotFoundException.java new file mode 100644 index 0000000..0d96327 --- /dev/null +++ b/src/main/java/ru/oa2/lti/domain/exception/TaskNotFoundException.java @@ -0,0 +1,18 @@ +package ru.oa2.lti.domain.exception; + +import ru.oa2.lti.domain.valueobject.TaskId; + +import java.util.UUID; + +/** + * Исключение, когда задача не найдена + */ +public class TaskNotFoundException extends DomainException { + public TaskNotFoundException(TaskId taskId) { + super("Task not found with id: " + taskId.value()); + } + + public TaskNotFoundException(UUID contextId) { + super("Task not found for context: " + contextId); + } +} diff --git a/src/main/java/ru/oa2/lti/domain/model/Task.java b/src/main/java/ru/oa2/lti/domain/model/Task.java new file mode 100644 index 0000000..68cfd79 --- /dev/null +++ b/src/main/java/ru/oa2/lti/domain/model/Task.java @@ -0,0 +1,140 @@ +package ru.oa2.lti.domain.model; + +import ru.oa2.lti.domain.exception.InvalidTaskStateException; +import ru.oa2.lti.domain.valueobject.*; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +/** + * Доменная модель задачи (Aggregate Root). + * Содержит бизнес-логику и инварианты. + * НЕ зависит от инфраструктурных деталей (JPA, Spring, etc.) + */ +public class Task { + private final TaskId id; + private TaskName name; + private Description description; + private final Map scripts; + + // Приватный конструктор - создание только через фабричные методы + private Task(TaskId id, TaskName name, Description description) { + this.id = Objects.requireNonNull(id, "Task id cannot be null"); + this.name = Objects.requireNonNull(name, "Task name cannot be null"); + this.description = Objects.requireNonNull(description, "Task description cannot be null"); + this.scripts = new HashMap<>(); + } + + /** + * Фабричный метод для создания новой задачи + */ + public static Task create(TaskName name, Description description) { + return new Task(null, name, description); + } + + /** + * Фабричный метод для восстановления существующей задачи из хранилища + */ + public static Task restore(TaskId id, TaskName name, Description description) { + return new Task(id, name, description); + } + + /** + * Добавляет или обновляет скрипт определенного типа + */ + public void setScript(Script script) { + Objects.requireNonNull(script, "Script cannot be null"); + scripts.put(script.type(), script); + } + + /** + * Получает скрипт определенного типа + */ + public Optional