diff --git a/.gitignore b/.gitignore index d3bedd1..e4a6f6e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,24 +1,36 @@ -# Compiled class file -*.class +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ -# Log file -*.log -*.DS_Store +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache -# BlueJ files -*.ctxt +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +*.DS_Store -# Mobile Tools for Java (J2ME) -.mtj.tmp/ -# Package Files # -*.jar -*.war -*.nar -*.ear -*.zip -*.tar.gz -*.rar +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/image/** +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ -# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml -hs_err_pid* +### VS Code ### +.vscode/ diff --git a/.idea/amplicode-jpa.xml b/.idea/amplicode-jpa.xml index 98351bf..f6a56e2 100644 --- a/.idea/amplicode-jpa.xml +++ b/.idea/amplicode-jpa.xml @@ -1,6 +1,8 @@ - + + \ No newline at end of file diff --git a/.idea/amplicode-settings.xml b/.idea/amplicode-settings.xml new file mode 100644 index 0000000..b965efa --- /dev/null +++ b/.idea/amplicode-settings.xml @@ -0,0 +1,15 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..795ca79 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/dataSources.local.xml b/.idea/dataSources.local.xml new file mode 100644 index 0000000..10a6f99 --- /dev/null +++ b/.idea/dataSources.local.xml @@ -0,0 +1,20 @@ + + + + + + " + + + master_key + sa + + + + + + + + + + \ No newline at end of file diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml new file mode 100644 index 0000000..ab41c1c --- /dev/null +++ b/.idea/dataSources.xml @@ -0,0 +1,17 @@ + + + + + h2.unified + true + org.h2.Driver + jdbc:h2:mem:testdb + + + + + + $ProjectFileDir$ + + + \ No newline at end of file diff --git a/.idea/dataSources/f04243a9-2346-4975-b5d1-b9ee4bd49928.xml b/.idea/dataSources/f04243a9-2346-4975-b5d1-b9ee4bd49928.xml new file mode 100644 index 0000000..7a05fb5 --- /dev/null +++ b/.idea/dataSources/f04243a9-2346-4975-b5d1-b9ee4bd49928.xml @@ -0,0 +1,178 @@ + + + + + 2.2.220 + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + 2025-04-27.14:25:53 + + + 1 + + + \ No newline at end of file diff --git a/.idea/dataSources/f04243a9-2346-4975-b5d1-b9ee4bd49928/storage_v2/_src_/database/TESTDB.kGhKkw.meta b/.idea/dataSources/f04243a9-2346-4975-b5d1-b9ee4bd49928/storage_v2/_src_/database/TESTDB.kGhKkw.meta new file mode 100644 index 0000000..5d0718d --- /dev/null +++ b/.idea/dataSources/f04243a9-2346-4975-b5d1-b9ee4bd49928/storage_v2/_src_/database/TESTDB.kGhKkw.meta @@ -0,0 +1 @@ +#n:TESTDB \ No newline at end of file diff --git a/.idea/dataSources/f04243a9-2346-4975-b5d1-b9ee4bd49928/storage_v2/_src_/database/TESTDB.kGhKkw/schema/PUBLIC.aaZQjQ.meta b/.idea/dataSources/f04243a9-2346-4975-b5d1-b9ee4bd49928/storage_v2/_src_/database/TESTDB.kGhKkw/schema/PUBLIC.aaZQjQ.meta new file mode 100644 index 0000000..3fdc81a --- /dev/null +++ b/.idea/dataSources/f04243a9-2346-4975-b5d1-b9ee4bd49928/storage_v2/_src_/database/TESTDB.kGhKkw/schema/PUBLIC.aaZQjQ.meta @@ -0,0 +1,2 @@ +#n:PUBLIC +! [0, 0, null, null, -2147483648, -2147483648] diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..1926ee2 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..712ab9d --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 6a61e88..864598a 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,5 +1,22 @@ + + + + + + + diff --git a/README.md b/README.md index ffe84a5..70811df 100644 --- a/README.md +++ b/README.md @@ -2,97 +2,206 @@ ![Task-manager Banner](image_task-manager.jpeg) -![Java](https://img.shields.io/badge/Java-21-orange) +![Java](https://img.shields.io/badge/Java-21-orange) +![Spring](https://img.shields.io/badge/Spring-3.3-green) +![Redis](https://img.shields.io/badge/Redis-7.2-red) +![H2](https://img.shields.io/badge/H2-2.2-blue) +![Docker](https://img.shields.io/badge/Docker-27.0-blue) +![Docker Compose](https://img.shields.io/badge/Docker_Compose-3.8-blue) ![JUnit](https://img.shields.io/badge/JUnit-5-orange) +![Mockito](https://img.shields.io/badge/Mockito-5.12-yellow) --- ## 📝 Описание проекта -**java-task-manager** — это консольное приложение для управления задачами, созданное мной с нуля для упрощения планирования и отслеживания рабочего процесса. Это учебный проект, который помог мне отточить навыки работы с **Java Core** и разобраться в принципах управления задачами в памяти приложения. Это не просто программа — это инструмент, который помогает организовать хаос и привести дела в порядок. +**java-task-manager** — это современное веб-приложение для управления задачами, разработанное с использованием **Spring Boot**. Проект позволяет эффективно организовывать задачи, эпики и подзадачи, отслеживать их статус и историю просмотров. Это учебный проект, созданный для отработки навыков работы с **Spring Framework**, **JPA**, **Redis**, и другими технологиями, а также для реализации полноценного REST API с документацией через Swagger. **Почему я создал этот проект?** -- Чтобы научиться управлять задачами без внешних баз данных. -- Отработать работу с приоритетами и временными метками. -- Реализовать функционал истории просмотров задач. +- Чтобы изучить разработку RESTful-приложений с использованием Spring Boot. +- Освоить интеграцию с Redis для хранения истории просмотров. +- Практиковать тестирование с JUnit и Mockito. +- Реализовать контейнеризацию с помощью Docker и Docker Compose. **Какую проблему решает?** -Если вы хотите навести порядок в своих задачах или попрактиковаться в управлении временем, java-task-manager станет вашим помощником. Всё управление происходит в одном месте — просто и удобно. +Проект помогает структурировать рабочие процессы, предоставляя удобный REST API для управления задачами, эпиками и подзадачами. Он подходит как для личного использования, так и для изучения современных подходов к разработке серверных приложений. **Технологии:** -- **Java 21** — ядро проекта. -- **HttpServer** и **HttpExchange** — для локального сервера. -- **JUnit 5** — для тестирования кода. +- **Java 21** — Язык программирования. +- **Spring Boot 3.3** — Основа приложения, включая Spring Web, Spring Data JPA. +- **Redis 7.2** — Для хранения истории просмотров. +- **H2 Database 2.2** — Встроенная база данных для хранения задач, эпиков и подзадач. +- **Docker** и **Docker Compose** — Для контейнеризации приложения и зависимостей. +- **JUnit 5** и **Mockito** — Для модульного и интеграционного тестирования. +- **MapStruct** — Для маппинга между DTO и сущностями. +- **Swagger (Springdoc)** — Для документации API. +- **Lombok** — Для упрощения кода. --- ## 🚀 Возможности -Вот что умеет **java-task-manager**: -- **Управление задачами:** - - Создание, обновление и удаление задач (всё хранится в памяти). -- **Время и приоритеты:** Привязка задач ко времени и установка уровней приоритета. +**java-task-manager** предоставляет следующие функции: +- **Управление задачами, эпиками и подзадачами:** + - Создание, обновление, получение и удаление через REST API. + - Поддержка временных меток (`startTime`, `endTime`, `duration`) и статусов (`NEW`, `IN_PROGRESS`, `DONE`). - **Типы задач:** - - **Эпики** — крупные задачи. - - **Задачи** — основные единицы работы. - - **Подзадачи** — детализация эпиков. -- **История просмотров:** Отслеживание задач, которые вы просматривали. -- **Локальный сервер:** Работает на порту 8080. -- **Эндпоинты API:** - - `/tasks` — список всех задач. - - `/epics` — работа с эпиками. - - `/subtasks` — управление подзадачами. - - `/history` — история просмотров. - - `/prioritized` — задачи по приоритету. + - **Эпики** — Крупные задачи, содержащие подзадачи. + - **Задачи** — Независимые единицы работы. + - **Подзадачи** — Детализация эпиков, связанные с ними. +- **Приоритизация:** Получение списков задач, эпиков и подзадач, отсортированных по статусу (`IN_PROGRESS` → `NEW` → `DONE`) и времени завершения. +- **История просмотров:** Отслеживание последних 10 просмотров задач, эпиков и подзадач (хранится в Redis). +- **REST API:** + - `/task` — Управление задачами. + - `/epic` — Управление эпиками. + - `/subtask` — Управление подзадачами. + - `/history` — Получение истории просмотров. + - `/prioritized` — Получение приоритетных списков. +- **Документация API:** Доступна через Swagger UI (`/swagger-ui.html`). +- **Тестирование:** Написаны тесты для контроллеров и сервисов с использованием JUnit и Mockito. +- **Контейнеризация:** Возможность запуска приложения и Redis в Docker через Docker Compose. --- ## 🛠️ Как запустить проект ### Требования -- **Java 21** или выше. -- Любая операционная система: Windows, macOS, Linux. - -### Инструкция -1. **Склонируйте репозиторий:** +- **Java 21** или выше (для локального запуска). +- **Docker** и **Docker Compose** (для запуска в контейнерах). +- **Maven** (для сборки проекта). +- Операционная система: Windows, macOS, Linux. + +### Инструкция для локального запуска +1. **Склонируйте репозиторий:** + ```bash + git clone https://github.com/1EVILGUN1/java-task-manager.git + ``` +2. **Перейдите в папку проекта:** + ```bash + cd java-task-manager + ``` +3. **Убедитесь, что Redis запущен локально или в Docker:** + ```bash + docker run -d --name redis -p 6379:6379 redis:7.2 + ``` +4. **Соберите проект с помощью Maven:** + ```bash + mvn clean install + ``` +5. **Запустите приложение:** + ```bash + mvn spring-boot:run + ``` +6. **Проверьте работу:** + - Откройте браузер и перейдите на `http://localhost:8080/swagger-ui.html` для доступа к документации API. + - Используйте эндпоинты, например, `http://localhost:8080/task` для работы с задачами. + +### Инструкция для запуска с Docker Compose +1. **Склонируйте репозиторий (если ещё не сделано):** + ```bash git clone https://github.com/1EVILGUN1/java-task-manager.git -2. **Перейдите в папку проекта:** + ``` +2. **Перейдите в папку проекта:** + ```bash cd java-task-manager -3. **Запустите приложение:** - - В среде разработки (IntelliJ IDEA, Eclipse, VSCode): найдите файл `Main.java` и нажмите "Run". - - Или через терминал: - javac Main.java - java Main -4. **Проверьте работу:** - Откройте браузер и перейдите на `http://localhost:8080`. + ``` +3. **Запустите приложение и Redis с помощью Docker Compose:** + ```bash + docker-compose up -d --build + ``` +4. **Проверьте работу:** + - Перейдите на `http://localhost:8080/swagger-ui.html` для проверки API. + - Для остановки: + ```bash + docker-compose down + ``` + +--- + +## 🧪 Тестирование -Если всё сделано верно, сервер запустится, и вы сможете взаимодействовать с задачами! +Проект включает тесты для: +- **Контроллеров**: Проверка эндпоинтов с использованием `@WebMvcTest` и Mockito для имитации сервисов. +- **Сервисов**: Проверка бизнес-логики с использованием `@SpringBootTest` и Mockito для имитации репозиториев. + +Запуск тестов: +```bash +mvn test +``` --- ## 📋 Планы на будущее -Я планирую развивать проект дальше: -- Добавить больше тестов с использованием **JUnit**. -- Улучшить производительность работы с задачами в памяти. -- Расширить API, например, добавить эндпоинт `/tasks/completed` для завершенных задач. +- Добавить эндпоинт `/tasks/completed` для получения завершенных задач. +- Реализовать аутентификацию и авторизацию через Spring Security. +- Добавить поддержку PostgreSQL как альтернативной базы данных. +- Расширить тестовое покрытие, включая интеграционные тесты для работы с Redis. +- Оптимизировать производительность сортировки в методе `/prioritized`. +- Добавить CI/CD через GitHub Actions для автоматической сборки и тестирования. --- ## 🌟 Почему стоит попробовать? -- **Простота:** Минимум зависимостей, всё в одном месте. -- **Практичность:** Удобное управление задачами и приоритетами. -- **Обучение:** Отличный пример для изучения Java Core. +- **Мощный стек технологий:** Spring Boot, Redis, H2, Docker — всё, что нужно для современного приложения. +- **Полноценный REST API:** Удобный интерфейс с документацией через Swagger. +- **Тестирование:** Надежный код с покрытием тестами для контроллеров и сервисов. +- **Контейнеризация:** Легкий запуск приложения и Redis с помощью Docker Compose. +- **Обучение:** Отличный пример для изучения Spring Framework и микросервисной архитектуры. --- ## 📬 Контакты -- **Email:** [yasha.golotin@mail.ru](mailto:yasha.golotin.mail.ru) +- **Email:** [yasha.golotin@mail.ru](mailto:yasha.golotin@mail.ru) --- ## 🤝 Как помочь проекту -Если хотите внести свой вклад, я буду рад! Форкните репозиторий, предложите свои идеи через pull request и присоединяйтесь к развитию java-task-manager. \ No newline at end of file +Хотите внести свой вклад? Форкните репозиторий, предложите улучшения через pull request или поделитесь идеями! Возможные направления: +- Расширение функционала API. +- Улучшение тестового покрытия. +- Оптимизация работы с Redis или H2. +- Улучшение `docker-compose.yml` или Dockerfile. + +Присоединяйтесь к развитию **java-task-manager**! + +--- + +## 📚 Структура проекта + +- **`src/main/java/service/task.manager`**: + - `controller` — REST-контроллеры для обработки HTTP-запросов. + - `dto` — Объекты передачи данных (DTO) для задач, эпиков и подзадач. + - `exception` — Пользовательские исключения и обработчик ошибок. + - `mapper` — MapStruct-мапперы для преобразования между DTO и сущностями. + - `model` — JPA-сущности (`Task`, `Epic`, `Subtask`) и перечисления. + - `repository` — Spring Data JPA репозитории для работы с базой данных. + - `service` — Интерфейсы и реализации бизнес-логики. +- **`src/test`** — Тесты для контроллеров и сервисов (JUnit, Mockito). +- **`Dockerfile`** — Для сборки образа приложения. +- **`docker-compose.yml`** — Для запуска приложения и Redis. + +--- + +## 🐳 Примечания по Docker + +- **Dockerfile**: Настроен для сборки Spring Boot приложения на базе `eclipse-temurin:21-jre-jammy`. +- **Docker Compose**: Автоматически запускает приложение и Redis, связывая их через сеть `task-manager-network`. +- **Конфигурация**: Настройки Redis и H2 задаются через переменные окружения в `docker-compose.yml` или `application.properties`. + +--- + +## 🔗 Полезные ссылки + +- [Swagger UI](http://localhost:8080/swagger-ui.html) — Документация API. +- [Spring Boot](https://spring.io/projects/spring-boot) — Документация фреймворка. +- [Redis](https://redis.io) — Документация по Redis. +- [Docker](https://docs.docker.com) — Руководство по Docker. +- [Docker Compose](https://docs.docker.com/compose/) — Документация по Docker Compose. + +--- + +**java-task-manager** — это не просто менеджер задач, это шаг к освоению современных технологий разработки! 🚀 \ No newline at end of file diff --git a/amplicode.xml b/amplicode.xml new file mode 100644 index 0000000..50bc959 --- /dev/null +++ b/amplicode.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..7e1e37c --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,39 @@ +services: + task-manager: + build: + context: . + dockerfile: Dockerfile + image: java-task-manager:latest + container_name: task-manager + ports: + - "8080:8080" + environment: + - JAVA_OPTS=-Xms512m -Xmx512m + - SPRING_REDIS_HOST=redis + - SPRING_REDIS_PORT=6379 + - SPRING_DATASOURCE_URL=jdbc:h2:mem:taskmanager;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE + - SPRING_DATASOURCE_DRIVER_CLASS_NAME=org.h2.Driver + - SPRING_DATASOURCE_USERNAME=sa + - SPRING_DATASOURCE_PASSWORD= + - SPRING_JPA_HIBERNATE_DDL_AUTO=update + depends_on: + - redis + networks: + - task-manager-network + + redis: + image: redis:7.2 + container_name: redis + ports: + - "6379:6379" + volumes: + - redis-data:/data + networks: + - task-manager-network + +networks: + task-manager-network: + driver: bridge + +volumes: + redis-data: \ No newline at end of file diff --git a/resources/task.csv b/resources/task.csv deleted file mode 100644 index f86272e..0000000 --- a/resources/task.csv +++ /dev/null @@ -1,4 +0,0 @@ -id,type,name,status,description,epic -1,TASK,Дом,NEW,Убраться в кухни и ванной,2024-06-19T20:18:39.706859,2024-06-19T20:58:39.706859 -2,TASK,Работа,IN_PROGRESS,Сделать куча рутины и пойти домой:),2024-06-20T20:18:39.719817,2024-06-20T21:08:39.719817 - diff --git a/service/Dockerfile b/service/Dockerfile new file mode 100644 index 0000000..e2225cf --- /dev/null +++ b/service/Dockerfile @@ -0,0 +1,5 @@ +FROM eclipse-temurin:21-jre-jammy +VOLUME /tmp +ARG JAR_FILE=target/*.jar +COPY ${JAR_FILE} service.jar +ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /service.jar"] \ No newline at end of file diff --git a/service/pom.xml b/service/pom.xml new file mode 100644 index 0000000..2ca9bc6 --- /dev/null +++ b/service/pom.xml @@ -0,0 +1,137 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.2.5 + + + service.task.manager + service + 1.0-SNAPSHOT + + + 21 + 21 + UTF-8 + 2.6.0 + 1.6.0 + 1.18.34 + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-data-redis + + + org.mockito + mockito-core + 5.12.0 + test + + + org.mockito + mockito-junit-jupiter + 5.12.0 + test + + + + + + + + org.projectlombok + lombok + 1.18.34 + provided + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-starter-validation + + + com.h2database + h2 + runtime + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + ${springdoc.version} + + + org.mapstruct + mapstruct + ${mapstruct.version} + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 21 + 21 + + + + org.projectlombok + lombok + ${lombok.version} + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.6.0 + + + generate-javadoc + generate-resources + + javadoc + + + + + 21 + none + ${project.basedir}/docs/javadoc + + + + + + \ No newline at end of file diff --git a/service/src/main/java/service/task/manager/TaskManagerApplication.java b/service/src/main/java/service/task/manager/TaskManagerApplication.java new file mode 100644 index 0000000..acb9e68 --- /dev/null +++ b/service/src/main/java/service/task/manager/TaskManagerApplication.java @@ -0,0 +1,11 @@ +package service.task.manager; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class TaskManagerApplication { + public static void main(String[] args) { + SpringApplication.run(TaskManagerApplication.class, args); + } +} \ No newline at end of file diff --git a/service/src/main/java/service/task/manager/config/RedisConfig.java b/service/src/main/java/service/task/manager/config/RedisConfig.java new file mode 100644 index 0000000..74d82ec --- /dev/null +++ b/service/src/main/java/service/task/manager/config/RedisConfig.java @@ -0,0 +1,20 @@ +package service.task.manager.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; +import service.task.manager.model.HistoryEntry; + +@Configuration +public class RedisConfig { + + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(connectionFactory); + template.setDefaultSerializer(new Jackson2JsonRedisSerializer<>(HistoryEntry.class)); + return template; + } +} \ No newline at end of file diff --git a/service/src/main/java/service/task/manager/controller/EpicController.java b/service/src/main/java/service/task/manager/controller/EpicController.java new file mode 100644 index 0000000..aa386bd --- /dev/null +++ b/service/src/main/java/service/task/manager/controller/EpicController.java @@ -0,0 +1,109 @@ +package service.task.manager.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import service.task.manager.dto.epic.EpicRequestCreatedDto; +import service.task.manager.dto.epic.EpicRequestUpdatedDto; +import service.task.manager.dto.epic.EpicResponseDto; +import service.task.manager.service.EpicService; + +import java.util.List; + +@Slf4j +@CrossOrigin +@RestController +@RequiredArgsConstructor +@RequestMapping("/epic") +@Tag(name = "Epic API", description = "API for managing epics") +public class EpicController { + private final EpicService service; + + @PostMapping + @Operation(summary = "Create a new epic", description = "Creates a new epic with the provided data.") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Epic created successfully"), + @ApiResponse(responseCode = "409", description = "Epic with the same name already exists") + }) + public ResponseEntity create(@RequestBody @Valid EpicRequestCreatedDto dto) { + log.info("Creating epic with name: {}", dto.name()); + service.create(dto); + return ResponseEntity.status(HttpStatus.CREATED).build(); + } + + @PutMapping + @Operation(summary = "Update an existing epic", description = "Updates an epic with the provided data.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Epic updated successfully"), + @ApiResponse(responseCode = "404", description = "Epic not found") + }) + public ResponseEntity update(@RequestBody @Valid EpicRequestUpdatedDto dto) { + log.info("Updating epic with ID: {}", dto.id()); + EpicResponseDto updatedEpic = service.update(dto); + return ResponseEntity.ok(updatedEpic); + } + + @GetMapping("/{id}") + @Operation(summary = "Get an epic by ID", description = "Retrieves an epic by its ID, including its subtasks.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Epic retrieved successfully"), + @ApiResponse(responseCode = "404", description = "Epic not found") + }) + public ResponseEntity findById( + @Parameter(description = "ID of the epic to retrieve") @PathVariable @Positive(message = "id must be positive") + @NotNull(message = "null id") Long id) { + log.info("Fetching epic with ID: {}", id); + EpicResponseDto epic = service.findById(id); + return ResponseEntity.ok(epic); + } + + @GetMapping + @Operation(summary = "Get all epics", description = "Retrieves a list of all epics.") + @ApiResponse(responseCode = "200", description = "List of epics retrieved successfully") + public ResponseEntity> findAll() { + log.info("Fetching all epics"); + List epics = service.findAll(); + return ResponseEntity.ok(epics); + } + + @DeleteMapping("/{id}") + @Operation(summary = "Delete an epic by ID", description = "Deletes an epic by its ID.") + @ApiResponses(value = { + @ApiResponse(responseCode = "204", description = "Epic deleted successfully"), + @ApiResponse(responseCode = "404", description = "Epic not found") + }) + public ResponseEntity delete( + @Parameter(description = "ID of the epic to delete") @PathVariable @Positive(message = "id must be positive") + @NotNull(message = "null id") Long id) { + log.info("Deleting epic with ID: {}", id); + service.delete(id); + return ResponseEntity.noContent().build(); + } + + @GetMapping("/prioritized") + @Operation( + summary = "Retrieve prioritized epics", + description = "Returns a list of all epics sorted by priority: IN_PROGRESS first, then NEW, and finally DONE. " + + "Within each status, epics are sorted by end time (earliest first)." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Successfully retrieved the list of prioritized epics"), + @ApiResponse(responseCode = "500", + description = "Internal server exception") + }) + public ResponseEntity> prioritized() { + log.info("Fetching prioritized epics"); + return ResponseEntity.ok(service.prioritized()); + } +} \ No newline at end of file diff --git a/service/src/main/java/service/task/manager/controller/HistoryController.java b/service/src/main/java/service/task/manager/controller/HistoryController.java new file mode 100644 index 0000000..4b39be1 --- /dev/null +++ b/service/src/main/java/service/task/manager/controller/HistoryController.java @@ -0,0 +1,60 @@ +package service.task.manager.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import service.task.manager.model.HistoryEntry; +import service.task.manager.service.HistoryService; + +import java.util.List; + +/** + * Controller for managing and retrieving the history of task accesses. + */ +@Slf4j +@CrossOrigin +@RestController +@RequiredArgsConstructor +@RequestMapping("/history") +@Tag(name = "History API", description = "API for retrieving the history of task accesses") +public class HistoryController { + + private final HistoryService service; + + /** + * Retrieves the history of task accesses. + * The history contains the last 10 records of calls to the findBy(long id) method for tasks, epics, and subtasks. + * + * @return a list of history entries + */ + @GetMapping + @Operation(summary = "Get task access history", description = "Retrieves the history of the last 10 task accesses (Task, Epic, Subtask) made via the findBy(long id) method.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "History retrieved successfully", + content = @Content(mediaType = "application/json", + array = @ArraySchema(schema = @Schema(implementation = HistoryEntry.class)))), + @ApiResponse(responseCode = "500", description = "Internal server exception, possibly due to Redis connectivity issues", + content = @Content) + }) + public List getHistory() { + try { + log.info("Received request to retrieve task access history"); + List history = service.getHistory(); + log.debug("Successfully retrieved history with {} entries", history.size()); + return history; + } catch (Exception e) { + log.error("Failed to retrieve task access history: {}", e.getMessage(), e); + throw new RuntimeException("Failed to retrieve history", e); + } + } +} \ No newline at end of file diff --git a/service/src/main/java/service/task/manager/controller/README.md b/service/src/main/java/service/task/manager/controller/README.md new file mode 100644 index 0000000..9fe2ab7 --- /dev/null +++ b/service/src/main/java/service/task/manager/controller/README.md @@ -0,0 +1,105 @@ +# README.md для папки `controller` + +## Описание + +Папка `controller` содержит REST-контроллеры приложения `service.task.manager`, разработанного на Spring Boot. Контроллеры обрабатывают HTTP-запросы для управления задачами, эпиками, подзадачами и историей доступа. Все эндпоинты задокументированы с использованием аннотаций OpenAPI (Swagger), что упрощает интеграцию и тестирование API. Логирование реализовано через SLF4J, а валидация данных — через Jakarta Validation. + +## Структура папки + +Папка включает следующие контроллеры: + +### 1. `EpicController.java` +**Описание**: Управляет эпиками — крупными задачами, содержащими подзадачи. +**Эндпоинты**: +- `POST /epic` — Создание нового эпика. +- `PUT /epic` — Обновление существующего эпика. +- `GET /epic/{id}` — Получение эпика по ID (включая подзадачи). +- `GET /epic` — Получение списка всех эпиков. +- `DELETE /epic/{id}` — Удаление эпика по ID. +- `GET /epic/prioritized` — Получение эпиков, отсортированных по приоритету (`IN_PROGRESS`, `NEW`, `DONE`) и времени завершения. + +### 2. `HistoryController.java` +**Описание**: Предоставляет доступ к истории вызовов метода `findById` для задач, эпиков и подзадач. +**Эндпоинты**: +- `GET /history` — Получение последних 10 записей истории доступа. + +### 3. `SubtaskController.java` +**Описание**: Управляет подзадачами, связанными с эпиками. +**Эндпоинты**: +- `POST /subtask` — Создание новой подзадачи. +- `PUT /subtask` — Обновление существующей подзадачи. +- `GET /subtask/{id}` — Получение подзадачи по ID. +- `GET /subtask` — Получение списка всех подзадач. +- `DELETE /subtask/{id}` — Удаление подзадачи по ID. +- `GET /subtask/prioritized` — Получение подзадач, отсортированных по приоритету (`IN_PROGRESS`, `NEW`, `DONE`) и времени завершения. + +### 4. `TaskController.java` +**Описание**: Управляет задачами — основными единицами работы в системе. +**Эндпоинты**: +- `POST /task` — Создание новой задачи. +- `PUT /task` — Обновление существующей задачи. +- `GET /task/{id}` — Получение задачи по ID. +- `GET /task` — Получение списка всех задач. +- `DELETE /task/{id}` — Удаление задачи по ID. +- `GET /task/prioritized` — Получение задач, отсортированных по приоритету (`IN_PROGRESS`, `NEW`, `DONE`) и времени завершения. + +## Основные особенности + +- **Документация API**: Эндпоинты аннотированы с помощью `@Operation`, `@ApiResponses`, `@Tag` для генерации спецификации OpenAPI. Документация доступна через Swagger UI по пути `/swagger-ui.html`. +- **Логирование**: Используется SLF4J (`@Slf4j`) для записи операций (создание, обновление, удаление, получение) в логи. +- **Валидация данных**: Входящие запросы проверяются с помощью аннотаций `@Valid`, `@NotNull`, `@Positive`. +- **CORS**: Поддержка кросс-доменных запросов через `@CrossOrigin`. +- **Коды HTTP-ответов**: + - `201 Created` — Успешное создание ресурса. + - `200 OK` — Успешное получение или обновление ресурса. + - `204 No Content` — Успешное удаление ресурса. + - `404 Not Found` — Ресурс не найден. + - `409 Conflict` — Конфликт (например, дублирование имени). + - `500 Internal Server Error` — Внутренняя ошибка (например, проблемы с Redis в `HistoryController`). +- **Сортировка по приоритету**: Эндпоинты `/prioritized` возвращают списки, отсортированные по статусу (`IN_PROGRESS` → `NEW` → `DONE`) и времени завершения (от раннего к позднему). + +## Зависимости + +Контроллеры используют сервисы, инжектируемые через конструктор (`@RequiredArgsConstructor`): +- `EpicService` — для работы с эпиками. +- `HistoryService` — для работы с историей доступа (зависит от Redis). +- `SubtaskService` — для работы с подзадачами. +- `TaskService` — для работы с задачами. + +## Документация API + +API документируется с использованием Springdoc OpenAPI. Доступ к документации: +- **Swagger UI**: `http://localhost:8080/swagger-ui.html` +- **OpenAPI JSON**: `http://localhost:8080/v3/api-docs` + +Конфигурация Springdoc: +- Название API: **Task Manager API** +- Версия: **1.0.0** +- Сортировка: Теги и операции сортируются по алфавиту (`tags-sorter: alpha`, `operations-sorter: alpha`). + +## Пример использования + +Контроллеры предназначены для взаимодействия с клиентскими приложениями через REST API. Для тестирования используйте Swagger UI или инструменты вроде `curl`. + +**Пример создания эпика**: +```bash +curl -X POST http://localhost:8080/epic \ +-H "Content-Type: application/json" \ +-d '{"name": "Новый эпик", "description": "Описание эпика"}' +``` + +**Пример получения истории**: +```bash +curl -X GET http://localhost:8080/history +``` + +## Примечания + +- **Redis**: Для работы `HistoryController` требуется подключение к Redis, так как история хранится в кэше. +- **DTO**: Контроллеры используют объекты передачи данных (DTO) для строгой типизации и валидации. +- **RESTful-дизайн**: Эндпоинты следуют принципам REST, обеспечивая удобное взаимодействие. +- **Swagger UI**: Документация API доступна по пути `/swagger-ui.html` после запуска приложения. + +## Дополнительно + +Для подробной информации о структуре DTO или сервисах см. папки `dto` и `service`. По вопросам настройки или расширения API обращайтесь к документации проекта или разработчикам. \ No newline at end of file diff --git a/service/src/main/java/service/task/manager/controller/SubtaskController.java b/service/src/main/java/service/task/manager/controller/SubtaskController.java new file mode 100644 index 0000000..f5e9b56 --- /dev/null +++ b/service/src/main/java/service/task/manager/controller/SubtaskController.java @@ -0,0 +1,110 @@ +package service.task.manager.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import service.task.manager.dto.subtask.SubtaskRequestCreatedDto; +import service.task.manager.dto.subtask.SubtaskRequestUpdatedDto; +import service.task.manager.dto.subtask.SubtaskResponseDto; +import service.task.manager.service.SubtaskService; + +import java.util.List; + +@Slf4j +@CrossOrigin +@RestController +@RequiredArgsConstructor +@RequestMapping("/subtask") +@Tag(name = "Subtask API", description = "API for managing subtasks") +public class SubtaskController { + private final SubtaskService service; + + @PostMapping + @Operation(summary = "Create a new subtask", description = "Creates a new subtask with the provided data, associated with an epic.") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Subtask created successfully"), + @ApiResponse(responseCode = "404", description = "Associated epic not found"), + @ApiResponse(responseCode = "409", description = "Subtask with the same name already exists") + }) + public ResponseEntity create(@RequestBody @Valid SubtaskRequestCreatedDto dto) { + log.info("Creating subtask with name: {}", dto.name()); + service.create(dto); + return ResponseEntity.status(HttpStatus.CREATED).build(); + } + + @PutMapping + @Operation(summary = "Update an existing subtask", description = "Updates an existing subtask with the provided data.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Subtask updated successfully"), + @ApiResponse(responseCode = "404", description = "Subtask not found") + }) + public ResponseEntity update(@RequestBody @Valid SubtaskRequestUpdatedDto dto) { + log.info("Updating subtask with ID: {}", dto.id()); + SubtaskResponseDto updatedSubtask = service.update(dto); + return ResponseEntity.ok(updatedSubtask); + } + + @GetMapping("/{id}") + @Operation(summary = "Get a subtask by ID", description = "Retrieves a subtask by its ID.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Subtask retrieved successfully"), + @ApiResponse(responseCode = "404", description = "Subtask not found") + }) + public ResponseEntity findById( + @Parameter(description = "ID of the subtask to retrieve") @PathVariable @Positive(message = "id must be positive") + @NotNull(message = "null id") Long id) { + log.info("Fetching subtask with ID: {}", id); + SubtaskResponseDto subtask = service.findById(id); + return ResponseEntity.ok(subtask); + } + + @GetMapping + @Operation(summary = "Get all subtasks", description = "Retrieves a list of all subtasks.") + @ApiResponse(responseCode = "200", description = "List of subtasks retrieved successfully") + public ResponseEntity> findAll() { + log.info("Fetching all subtasks"); + List subtasks = service.findAll(); + return ResponseEntity.ok(subtasks); + } + + @DeleteMapping("/{id}") + @Operation(summary = "Delete a subtask by ID", description = "Deletes a subtask by its ID.") + @ApiResponses(value = { + @ApiResponse(responseCode = "204", description = "Subtask deleted successfully"), + @ApiResponse(responseCode = "404", description = "Subtask not found") + }) + public ResponseEntity delete( + @Parameter(description = "ID of the subtask to delete") @PathVariable @Positive(message = "id must be positive") + @NotNull(message = "null id") Long id) { + log.info("Deleting subtask with ID: {}", id); + service.delete(id); + return ResponseEntity.noContent().build(); + } + + @GetMapping("/prioritized") + @Operation( + summary = "Retrieve prioritized subtasks", + description = "Returns a list of all subtasks sorted by priority: IN_PROGRESS first, then NEW, and finally DONE. " + + "Within each status, subtasks are sorted by end time (earliest first)." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Successfully retrieved the list of prioritized subtasks"), + @ApiResponse(responseCode = "500", + description = "Internal server exception") + }) + public ResponseEntity> prioritized() { + log.info("Fetching prioritized subtasks"); + return ResponseEntity.ok(service.prioritized()); + } +} \ No newline at end of file diff --git a/service/src/main/java/service/task/manager/controller/TaskController.java b/service/src/main/java/service/task/manager/controller/TaskController.java new file mode 100644 index 0000000..c37f739 --- /dev/null +++ b/service/src/main/java/service/task/manager/controller/TaskController.java @@ -0,0 +1,109 @@ +package service.task.manager.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import service.task.manager.dto.task.TaskRequestCreatedDto; +import service.task.manager.dto.task.TaskRequestUpdatedDto; +import service.task.manager.dto.task.TaskResponseDto; +import service.task.manager.service.TaskService; + +import java.util.List; + +@Slf4j +@CrossOrigin +@RestController +@RequiredArgsConstructor +@RequestMapping("/task") +@Tag(name = "Task API", description = "API for managing tasks") +public class TaskController { + private final TaskService service; + + @PostMapping + @Operation(summary = "Create a new task", description = "Creates a new task with the provided data.") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Task created successfully"), + @ApiResponse(responseCode = "409", description = "Task with the same name already exists") + }) + public ResponseEntity create(@RequestBody @Valid TaskRequestCreatedDto dto) { + log.info("Creating task with name: {}", dto.name()); + service.create(dto); + return ResponseEntity.status(HttpStatus.CREATED).build(); + } + + @PutMapping + @Operation(summary = "Update an existing task", description = "Updates an existing task with the provided data.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Task updated successfully"), + @ApiResponse(responseCode = "404", description = "Task not found") + }) + public ResponseEntity update(@RequestBody @Valid TaskRequestUpdatedDto dto) { + log.info("Updating task with ID: {}", dto.id()); + TaskResponseDto updatedTask = service.update(dto); + return ResponseEntity.ok(updatedTask); + } + + @GetMapping("/{id}") + @Operation(summary = "Get a task by ID", description = "Retrieves a task by its ID.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Task retrieved successfully"), + @ApiResponse(responseCode = "404", description = "Task not found") + }) + public ResponseEntity get( + @Parameter(description = "ID of the task to retrieve") @PathVariable @Positive(message = "id must be positive") + @NotNull(message = "null id") Long id) { + log.info("Fetching task with ID: {}", id); + TaskResponseDto task = service.findById(id); + return ResponseEntity.ok(task); + } + + @GetMapping + @Operation(summary = "Get all tasks", description = "Retrieves a list of all tasks.") + @ApiResponse(responseCode = "200", description = "List of tasks retrieved successfully") + public ResponseEntity> getAll() { + log.info("Fetching all tasks"); + List tasks = service.findAll(); + return ResponseEntity.ok(tasks); + } + + @DeleteMapping("/{id}") + @Operation(summary = "Delete a task by ID", description = "Deletes a task by its ID.") + @ApiResponses(value = { + @ApiResponse(responseCode = "204", description = "Task deleted successfully"), + @ApiResponse(responseCode = "404", description = "Task not found") + }) + public ResponseEntity delete( + @Parameter(description = "ID of the task to delete") @PathVariable @Positive(message = "id must be positive") + @NotNull(message = "null id") Long id) { + log.info("Deleting task with ID: {}", id); + service.delete(id); + return ResponseEntity.noContent().build(); + } + + @GetMapping("/prioritized") + @Operation( + summary = "Retrieve prioritized tasks", + description = "Returns a list of all tasks sorted by priority: IN_PROGRESS first, then NEW, and finally DONE. " + + "Within each status, tasks are sorted by end time (earliest first)." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Successfully retrieved the list of prioritized tasks"), + @ApiResponse(responseCode = "500", + description = "Internal server exception") + }) + public ResponseEntity> prioritized() { + log.info("Fetching prioritized subtasks"); + return ResponseEntity.ok(service.prioritized()); + } +} \ No newline at end of file diff --git a/service/src/main/java/service/task/manager/dto/README.md b/service/src/main/java/service/task/manager/dto/README.md new file mode 100644 index 0000000..391d701 --- /dev/null +++ b/service/src/main/java/service/task/manager/dto/README.md @@ -0,0 +1,153 @@ +# README.md для папки `dto` + +## Описание + +Папка `dto` содержит классы объектов передачи данных (Data Transfer Objects, DTO) для приложения `service.task.manager`. DTO используются для обмена данными между клиентскими запросами и серверной логикой, обеспечивая строгую типизацию, валидацию и документирование. Все DTO аннотированы с использованием OpenAPI (Swagger) для автоматической генерации документации API. Папка разделена на три подпапки: `task`, `epic` и `subtask`, каждая из которых отвечает за соответствующие сущности. + +## Структура папки + +### 1. Папка `epic` +Содержит DTO для работы с эпиками — крупными задачами, которые могут включать подзадачи. + +- **`EpicRequestCreatedDto.java`** + **Описание**: DTO для создания нового эпика. + **Поля**: + - `name` (`String`, обязательное, не пустое) — Название эпика. + - `description` (`String`, обязательное, не пустое) — Описание эпика. + - `startTime` (`LocalDateTime`, обязательное) — Время начала эпика. + - `duration` (`Duration`, обязательное) — Длительность эпика. + **Особенности**: Использует аннотацию `@Builder` для удобного создания объектов. + +- **`EpicRequestUpdatedDto.java`** + **Описание**: DTO для обновления существующего эпика. + **Поля**: + - `id` (`Long`, обязательное, положительное) — Идентификатор эпика. + - `name` (`String`, обязательное, не пустое) — Обновленное название. + - `description` (`String`, обязательное, не пустое) — Обновленное описание. + - `status` (`Status`, обязательное) — Обновленный статус (`NEW`, `IN_PROGRESS`, `DONE`). + - `duration` (`Duration`, обязательное) — Обновленная длительность. + +- **`EpicResponseDto.java`** + **Описание**: DTO для возврата данных об эпике, включая связанные подзадачи. + **Поля**: + - `id` (`Long`) — Идентификатор эпика. + - `subtasks` (`List`) — Список подзадач. + - `name` (`String`) — Название эпика. + - `description` (`String`) — Описание эпика. + - `status` (`Status`) — Статус эпика. + - `startTime` (`LocalDateTime`) — Время начала. + - `duration` (`Duration`) — Длительность. + - `endTime` (`LocalDateTime`) — Время окончания. + - `type` (`TaskType`) — Тип задачи (всегда `EPIC`). + **Внутренний рекорд**: + - `SubtaskDto` — Вложенный DTO для представления подзадач внутри эпика с полями `id`, `name`, `description`, `status`. + +### 2. Папка `subtask` +Содержит DTO для работы с подзадачами, которые связаны с эпиками. + +- **`SubtaskRequestCreatedDto.java`** + **Описание**: DTO для создания новой подзадачи. + **Поля**: + - `epicId` (`Long`, обязательное) — Идентификатор эпика, к которому привязана подзадача. + - `name` (`String`, обязательное, не пустое) — Название подзадачи. + - `description` (`String`, обязательное, не пустое) — Описание подзадачи. + - `startTime` (`LocalDateTime`, обязательное) — Время начала. + - `duration` (`Duration`, обязательное) — Длительность. + **Особенности**: Использует аннотацию `@Builder`. + +- **`SubtaskRequestUpdatedDto.java`** + **Описание**: DTO для обновления существующей подзадачи. + **Поля**: + - `id` (`Long`, обязательное, положительное) — Идентификатор подзадачи. + - `name` (`String`, обязательное, не пустое) — Обновленное название. + - `description` (`String`, обязательное, не пустое) — Обновленное описание. + - `status` (`Status`, обязательное) — Обновленный статус. + - `duration` (`Duration`, обязательное) — Обновленная длительность. + +- **`SubtaskResponseDto.java`** + **Описание**: DTO для возврата данных о подзадаче. + **Поля**: + - `id` (`Long`) — Идентификатор подзадачи. + - `epicId` (`Long`) — Идентификатор связанного эпика. + - `name` (`String`) — Название подзадачи. + - `description` (`String`) — Описание подзадачи. + - `status` (`Status`) — Статус подзадачи. + - `startTime` (`LocalDateTime`) — Время начала. + - `endTime` (`LocalDateTime`) — Время окончания. + - `duration` (`Duration`) — Длительность. + - `type` (`TaskType`) — Тип задачи (всегда `SUBTASK`). + +### 3. Папка `task` +Содержит DTO для работы с задачами — независимыми единицами работы. + +- **`TaskRequestCreatedDto.java`** + **Описание**: DTO для создания новой задачи. + **Поля**: + - `name` (`String`, обязательное, не пустое) — Название задачи. + - `description` (`String`, обязательное, не пустое) — Описание задачи. + - `startTime` (`LocalDateTime`, обязательное) — Время начала. + - `duration` (`Duration`, обязательное) — Длительность. + +- **`TaskRequestUpdatedDto.java`** + **Описание**: DTO для обновления существующей задачи. + **Поля**: + - `id` (`Long`, обязательное, положительное) — Идентификатор задачи. + - `name` (`String`, обязательное, не пустое) — Обновленное название. + - `description` (`String`, обязательное, не пустое) — Обновленное описание. + - `status` (`Status`, обязательное) — Обновленный статус. + - `duration` (`Duration`, обязательное) — Обновленная длительность. + +- **`TaskResponseDto.java`** + **Описание**: DTO для возврата данных о задаче. + **Поля**: + - `id` (`Long`) — Идентификатор задачи. + - `name` (`String`) — Название задачи. + - `description` (`String`) — Описание задачи. + - `status` (`Status`) — Статус задачи. + - `startTime` (`LocalDateTime`) — Время начала. + - `endTime` (`LocalDateTime`) — Время окончания. + - `duration` (`Duration`) — Длительность. + - `type` (`TaskType`) — Тип задачи (всегда `TASK`). + +## Основные особенности + +- **Документация**: Все DTO аннотированы с помощью `@Schema` для интеграции с Swagger. Поля содержат примеры и описания. +- **Валидация**: Используются аннотации Jakarta Validation (`@NotBlank`, `@NotNull`, `@Positive`) для проверки входных данных. +- **Типизация**: DTO реализованы как `record` для неизменяемости и компактности кода. +- **Перечисления**: + - `Status` (`NEW`, `IN_PROGRESS`, `DONE`) — для указания статуса задач, эпиков и подзадач. + - `TaskType` (`TASK`, `EPIC`, `SUBTASK`) — для идентификации типа сущности. +- **Временные данные**: Поля `startTime`, `endTime` и `duration` используют `LocalDateTime` и `Duration` для работы с временными интервалами. + +## Использование + +DTO используются в контроллерах (`controller`) для обработки запросов и ответов. Они обеспечивают: +- **Создание**: `RequestCreatedDto` для передачи данных при создании сущностей. +- **Обновление**: `RequestUpdatedDto` для изменения существующих сущностей. +- **Получение**: `ResponseDto` для возврата данных клиенту. + +**Пример JSON для создания эпика**: +```json +{ + "name": "Проектный план", + "description": "Планирование проекта", + "startTime": "2025-04-27T10:00:00", + "duration": "PT24H" +} +``` + +## Документация API + +DTO документируются через Springdoc OpenAPI. Доступ к документации: +- **Swagger UI**: `http://localhost:8080/swagger-ui.html` +- **OpenAPI JSON**: `http://localhost:8080/v3/api-docs` + +## Примечания + +- DTO разделены по сущностям (`task`, `epic`, `subtask`) для лучшей организации кода. +- Поля в `ResponseDto` включают дополнительные данные (например, `endTime`, `type`), которые вычисляются на сервере. +- Для корректной работы требуется наличие перечислений `Status` и `TaskType` в пакете `service.task.manager.model.enums`. + +## Дополнительно + +Для информации о контроллерах или сервисах см. папки `controller` и `service`. Если возникают вопросы по настройке или расширению DTO, обратитесь к документации проекта или разработчикам. \ No newline at end of file diff --git a/service/src/main/java/service/task/manager/dto/epic/EpicRequestCreatedDto.java b/service/src/main/java/service/task/manager/dto/epic/EpicRequestCreatedDto.java new file mode 100644 index 0000000..da54e47 --- /dev/null +++ b/service/src/main/java/service/task/manager/dto/epic/EpicRequestCreatedDto.java @@ -0,0 +1,33 @@ +package service.task.manager.dto.epic; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Builder; + +import java.time.Duration; +import java.time.LocalDateTime; + +/** + * DTO for creating a new epic. + */ +@Schema(description = "DTO for creating a new epic") +@Builder +public record EpicRequestCreatedDto( + @Schema(description = "Name of the epic", example = "Project Planning", required = true) + @NotBlank(message = "blank name") + String name, + + @Schema(description = "Description of the epic", example = "Planning phase of the project", required = true) + @NotBlank(message = "blank description") + String description, + + @Schema(description = "Start time of the epic", example = "2025-04-27T10:00:00", required = true) + @NotNull(message = "null start time") + LocalDateTime startTime, + + @Schema(description = "Duration of the epic", example = "PT24H", required = true) + @NotNull(message = "null duration") + Duration duration +) { +} \ No newline at end of file diff --git a/service/src/main/java/service/task/manager/dto/epic/EpicRequestUpdatedDto.java b/service/src/main/java/service/task/manager/dto/epic/EpicRequestUpdatedDto.java new file mode 100644 index 0000000..0fc68f4 --- /dev/null +++ b/service/src/main/java/service/task/manager/dto/epic/EpicRequestUpdatedDto.java @@ -0,0 +1,37 @@ +package service.task.manager.dto.epic; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import service.task.manager.model.enums.Status; + +import java.time.Duration; + +/** + * DTO for updating an existing epic. + */ +@Schema(description = "DTO for updating an existing epic") +public record EpicRequestUpdatedDto( + @Schema(description = "ID of the epic to update", example = "1", required = true) + @NotNull(message = "null id") + @Positive(message = "not positive id") + Long id, + + @Schema(description = "Updated name of the epic", example = "Updated Project Planning", required = true) + @NotBlank(message = "blank name") + String name, + + @Schema(description = "Updated description of the epic", example = "Updated planning phase", required = true) + @NotBlank(message = "blank description") + String description, + + @Schema(description = "Updated status of the epic", example = "IN_PROGRESS", required = true) + @NotNull(message = "null status") + Status status, + + @Schema(description = "Updated duration of the epic", example = "PT48H", required = true) + @NotNull(message = "null duration") + Duration duration +) { +} \ No newline at end of file diff --git a/service/src/main/java/service/task/manager/dto/epic/EpicResponseDto.java b/service/src/main/java/service/task/manager/dto/epic/EpicResponseDto.java new file mode 100644 index 0000000..8938b17 --- /dev/null +++ b/service/src/main/java/service/task/manager/dto/epic/EpicResponseDto.java @@ -0,0 +1,61 @@ +package service.task.manager.dto.epic; + +import io.swagger.v3.oas.annotations.media.Schema; +import service.task.manager.model.enums.Status; +import service.task.manager.model.enums.TaskType; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.List; + +/** + * DTO for retrieving an epic with its details and subtasks. + */ +@Schema(description = "DTO for retrieving an epic with its details and subtasks") +public record EpicResponseDto( + @Schema(description = "ID of the epic", example = "1") + Long id, + + @Schema(description = "List of subtasks associated with the epic") + List subtasks, + + @Schema(description = "Name of the epic", example = "Project Planning") + String name, + + @Schema(description = "Description of the epic", example = "Planning phase of the project") + String description, + + @Schema(description = "Status of the epic", example = "NEW") + Status status, + + @Schema(description = "Start time of the epic", example = "2025-04-27T10:00:00") + LocalDateTime startTime, + + @Schema(description = "Duration of the epic", example = "PT24H") + Duration duration, + + @Schema(description = "End time of the epic", example = "2025-04-28T10:00:00") + LocalDateTime endTime, + + @Schema(description = "Type of the task (always EPIC)", example = "EPIC") + TaskType type +) { + /** + * DTO for retrieving a subtask within an epic. + */ + @Schema(description = "DTO for retrieving a subtask within an epic") + public record SubtaskDto( + @Schema(description = "ID of the subtask", example = "1") + Long id, + + @Schema(description = "Name of the subtask", example = "Task 1") + String name, + + @Schema(description = "Description of the subtask", example = "First task in the epic") + String description, + + @Schema(description = "Status of the subtask", example = "NEW") + Status status + ) { + } +} \ No newline at end of file diff --git a/service/src/main/java/service/task/manager/dto/subtask/SubtaskRequestCreatedDto.java b/service/src/main/java/service/task/manager/dto/subtask/SubtaskRequestCreatedDto.java new file mode 100644 index 0000000..f4bda8e --- /dev/null +++ b/service/src/main/java/service/task/manager/dto/subtask/SubtaskRequestCreatedDto.java @@ -0,0 +1,37 @@ +package service.task.manager.dto.subtask; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Builder; + +import java.time.Duration; +import java.time.LocalDateTime; + +/** + * DTO for creating a new subtask. + */ +@Schema(description = "DTO for creating a new subtask") +@Builder +public record SubtaskRequestCreatedDto( + @Schema(description = "ID of the epic to which the subtask belongs", example = "1", required = true) + @NotNull(message = "null epic ID") + Long epicId, + + @Schema(description = "Name of the subtask", example = "Task 1", required = true) + @NotBlank(message = "blank name") + String name, + + @Schema(description = "Description of the subtask", example = "First task in the epic", required = true) + @NotBlank(message = "blank description") + String description, + + @Schema(description = "Start time of the subtask", example = "2025-04-27T10:00:00", required = true) + @NotNull(message = "null start time") + LocalDateTime startTime, + + @Schema(description = "Duration of the subtask", example = "PT24H", required = true) + @NotNull(message = "null duration") + Duration duration +) { +} \ No newline at end of file diff --git a/service/src/main/java/service/task/manager/dto/subtask/SubtaskRequestUpdatedDto.java b/service/src/main/java/service/task/manager/dto/subtask/SubtaskRequestUpdatedDto.java new file mode 100644 index 0000000..f258da5 --- /dev/null +++ b/service/src/main/java/service/task/manager/dto/subtask/SubtaskRequestUpdatedDto.java @@ -0,0 +1,37 @@ +package service.task.manager.dto.subtask; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import service.task.manager.model.enums.Status; + +import java.time.Duration; + +/** + * DTO for updating an existing subtask. + */ +@Schema(description = "DTO for updating an existing subtask") +public record SubtaskRequestUpdatedDto( + @Schema(description = "ID of the subtask to update", example = "1", required = true) + @NotNull(message = "null id") + @Positive(message = "not positive id") + Long id, + + @Schema(description = "Updated name of the subtask", example = "Updated Task 1", required = true) + @NotBlank(message = "blank name") + String name, + + @Schema(description = "Updated description of the subtask", example = "Updated first task", required = true) + @NotBlank(message = "blank description") + String description, + + @Schema(description = "Updated status of the subtask", example = "IN_PROGRESS", required = true) + @NotNull(message = "null status") + Status status, + + @Schema(description = "Updated duration of the subtask", example = "PT48H", required = true) + @NotNull(message = "null duration") + Duration duration +) { +} \ No newline at end of file diff --git a/service/src/main/java/service/task/manager/dto/subtask/SubtaskResponseDto.java b/service/src/main/java/service/task/manager/dto/subtask/SubtaskResponseDto.java new file mode 100644 index 0000000..dbcf06c --- /dev/null +++ b/service/src/main/java/service/task/manager/dto/subtask/SubtaskResponseDto.java @@ -0,0 +1,42 @@ +package service.task.manager.dto.subtask; + +import io.swagger.v3.oas.annotations.media.Schema; +import service.task.manager.model.enums.Status; +import service.task.manager.model.enums.TaskType; + +import java.time.Duration; +import java.time.LocalDateTime; + +/** + * DTO for retrieving a subtask with its details. + */ +@Schema(description = "DTO for retrieving a subtask with its details") +public record SubtaskResponseDto( + @Schema(description = "ID of the subtask", example = "1") + Long id, + + @Schema(description = "ID of the epic to which the subtask belongs", example = "1") + Long epicId, + + @Schema(description = "Name of the subtask", example = "Task 1") + String name, + + @Schema(description = "Description of the subtask", example = "First task in the epic") + String description, + + @Schema(description = "Status of the subtask", example = "NEW") + Status status, + + @Schema(description = "Start time of the subtask", example = "2025-04-27T10:00:00") + LocalDateTime startTime, + + @Schema(description = "End time of the subtask", example = "2025-04-28T10:00:00") + LocalDateTime endTime, + + @Schema(description = "Duration of the subtask", example = "PT24H") + Duration duration, + + @Schema(description = "Type of the task (always SUBTASK)", example = "SUBTASK") + TaskType type +) { +} \ No newline at end of file diff --git a/service/src/main/java/service/task/manager/dto/task/TaskRequestCreatedDto.java b/service/src/main/java/service/task/manager/dto/task/TaskRequestCreatedDto.java new file mode 100644 index 0000000..325b668 --- /dev/null +++ b/service/src/main/java/service/task/manager/dto/task/TaskRequestCreatedDto.java @@ -0,0 +1,31 @@ +package service.task.manager.dto.task; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +import java.time.Duration; +import java.time.LocalDateTime; + +/** + * DTO for creating a new task. + */ +@Schema(description = "DTO for creating a new task") +public record TaskRequestCreatedDto( + @Schema(description = "Name of the task", example = "Standalone Task", required = true) + @NotBlank(message = "blank name") + String name, + + @Schema(description = "Description of the task", example = "A standalone task", required = true) + @NotBlank(message = "blank description") + String description, + + @Schema(description = "Start time of the task", example = "2025-04-27T10:00:00", required = true) + @NotNull(message = "null start time") + LocalDateTime startTime, + + @Schema(description = "Duration of the task", example = "PT24H", required = true) + @NotNull(message = "null duration") + Duration duration +) { +} \ No newline at end of file diff --git a/service/src/main/java/service/task/manager/dto/task/TaskRequestUpdatedDto.java b/service/src/main/java/service/task/manager/dto/task/TaskRequestUpdatedDto.java new file mode 100644 index 0000000..8677443 --- /dev/null +++ b/service/src/main/java/service/task/manager/dto/task/TaskRequestUpdatedDto.java @@ -0,0 +1,37 @@ +package service.task.manager.dto.task; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import service.task.manager.model.enums.Status; + +import java.time.Duration; + +/** + * DTO for updating an existing task. + */ +@Schema(description = "DTO for updating an existing task") +public record TaskRequestUpdatedDto( + @Schema(description = "ID of the task to update", example = "1", required = true) + @NotNull(message = "null id") + @Positive(message = "not positive id") + Long id, + + @Schema(description = "Updated name of the task", example = "Updated Task", required = true) + @NotBlank(message = "blank name") + String name, + + @Schema(description = "Updated description of the task", example = "Updated standalone task", required = true) + @NotBlank(message = "blank description") + String description, + + @Schema(description = "Updated status of the task", example = "IN_PROGRESS", required = true) + @NotNull(message = "null status") + Status status, + + @Schema(description = "Updated duration of the task", example = "PT48H", required = true) + @NotNull(message = "null duration") + Duration duration +) { +} \ No newline at end of file diff --git a/service/src/main/java/service/task/manager/dto/task/TaskResponseDto.java b/service/src/main/java/service/task/manager/dto/task/TaskResponseDto.java new file mode 100644 index 0000000..0e60736 --- /dev/null +++ b/service/src/main/java/service/task/manager/dto/task/TaskResponseDto.java @@ -0,0 +1,39 @@ +package service.task.manager.dto.task; + +import io.swagger.v3.oas.annotations.media.Schema; +import service.task.manager.model.enums.Status; +import service.task.manager.model.enums.TaskType; + +import java.time.Duration; +import java.time.LocalDateTime; + +/** + * DTO for retrieving a task with its details. + */ +@Schema(description = "DTO for retrieving a task with its details") +public record TaskResponseDto( + @Schema(description = "ID of the task", example = "1") + Long id, + + @Schema(description = "Name of the task", example = "Standalone Task") + String name, + + @Schema(description = "Description of the task", example = "A standalone task") + String description, + + @Schema(description = "Status of the task", example = "NEW") + Status status, + + @Schema(description = "Start time of the task", example = "2025-04-27T10:00:00") + LocalDateTime startTime, + + @Schema(description = "End time of the task", example = "2025-04-28T10:00:00") + LocalDateTime endTime, + + @Schema(description = "Duration of the task", example = "PT24H") + Duration duration, + + @Schema(description = "Type of the task (always TASK)", example = "TASK") + TaskType type +) { +} \ No newline at end of file diff --git a/service/src/main/java/service/task/manager/exception/ConflictException.java b/service/src/main/java/service/task/manager/exception/ConflictException.java new file mode 100644 index 0000000..5588ba5 --- /dev/null +++ b/service/src/main/java/service/task/manager/exception/ConflictException.java @@ -0,0 +1,7 @@ +package service.task.manager.exception; + +public class ConflictException extends RuntimeException { + public ConflictException(String message) { + super(message); + } +} diff --git a/service/src/main/java/service/task/manager/exception/ErrorHandler.java b/service/src/main/java/service/task/manager/exception/ErrorHandler.java new file mode 100644 index 0000000..e19e2e6 --- /dev/null +++ b/service/src/main/java/service/task/manager/exception/ErrorHandler.java @@ -0,0 +1,88 @@ +package service.task.manager.exception; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; +import org.springframework.context.MessageSourceResolvable; +import org.springframework.http.HttpStatus; +import org.springframework.validation.FieldError; +import org.springframework.validation.ObjectError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.annotation.HandlerMethodValidationException; + +import java.util.HashMap; +import java.util.Map; + +@RestControllerAdvice +public class ErrorHandler { + + @ExceptionHandler(NotFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public ErrorResponse handleNotFoundException(NotFoundException ex) { + return new ErrorResponse(ex.getMessage()); + } + + @ExceptionHandler(InvalidTaskDataException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorResponse handleInvalidTaskDataException(InvalidTaskDataException ex) { + return new ErrorResponse(ex.getMessage()); + } + + @ExceptionHandler(TaskConstraintViolationException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorResponse handleTaskConstraintViolationException(TaskConstraintViolationException ex) { + return new ErrorResponse(ex.getMessage()); + } + + @ExceptionHandler(ConflictException.class) + @ResponseStatus(HttpStatus.CONFLICT) + public ErrorResponse handleConflictException(ConflictException ex) { + return new ErrorResponse(ex.getMessage()); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Map handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) { + Map errors = new HashMap<>(); + for (FieldError error : ex.getBindingResult().getFieldErrors()) { + errors.put(error.getField(), error.getDefaultMessage()); + } + return errors; + } + + @ExceptionHandler(ConstraintViolationException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Map handleConstraintViolationException(ConstraintViolationException ex) { + Map errors = new HashMap<>(); + for (ConstraintViolation violation : ex.getConstraintViolations()) { + String fieldName = violation.getPropertyPath().toString(); + fieldName = fieldName.substring(fieldName.lastIndexOf('.') + 1); + errors.put(fieldName, violation.getMessage()); + } + return errors; + } + + @ExceptionHandler(HandlerMethodValidationException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Map handleHandlerMethodValidationException(HandlerMethodValidationException ex) { + Map errors = new HashMap<>(); + for (MessageSourceResolvable resolvable : ex.getAllErrors()) { + if (resolvable instanceof FieldError fieldError) { + errors.put(fieldError.getField(), fieldError.getDefaultMessage()); + } else if (resolvable instanceof ObjectError objectError) { + // Для параметров метода (например, @PathVariable) + String paramName = objectError.getObjectName(); + errors.put(paramName, objectError.getDefaultMessage()); + } else { + // Для случаев, когда resolvable не является ObjectError + errors.put("error", resolvable.getDefaultMessage()); + } + } + return errors; + } + + record ErrorResponse(String message) { + } +} \ No newline at end of file diff --git a/service/src/main/java/service/task/manager/exception/InvalidTaskDataException.java b/service/src/main/java/service/task/manager/exception/InvalidTaskDataException.java new file mode 100644 index 0000000..8ef0b8c --- /dev/null +++ b/service/src/main/java/service/task/manager/exception/InvalidTaskDataException.java @@ -0,0 +1,7 @@ +package service.task.manager.exception; + +public class InvalidTaskDataException extends RuntimeException { + public InvalidTaskDataException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/service/src/main/java/service/task/manager/exception/NotFoundException.java b/service/src/main/java/service/task/manager/exception/NotFoundException.java new file mode 100644 index 0000000..6610a7b --- /dev/null +++ b/service/src/main/java/service/task/manager/exception/NotFoundException.java @@ -0,0 +1,7 @@ +package service.task.manager.exception; + +public class NotFoundException extends RuntimeException { + public NotFoundException(String message) { + super(message); + } +} diff --git a/service/src/main/java/service/task/manager/exception/README.md b/service/src/main/java/service/task/manager/exception/README.md new file mode 100644 index 0000000..d05e645 --- /dev/null +++ b/service/src/main/java/service/task/manager/exception/README.md @@ -0,0 +1,90 @@ +# README.md для папки `exception` + +## Описание + +Папка `exception` содержит классы исключений и обработчик ошибок для приложения `service.task.manager`. Эти компоненты обеспечивают централизованную обработку ошибок, возникающих при выполнении запросов к REST API, и возвращают клиенту понятные сообщения об ошибках с соответствующими HTTP-статусами. Исключения используются для обработки специфичных ошибок, таких как отсутствие ресурса, конфликты или некорректные данные, а обработчик (`ErrorHandler`) преобразует их в стандартизированные ответы. + +## Структура папки + +### 1. Исключения + +- **`ConflictException.java`** + **Описание**: Исключение, выбрасываемое при конфликте данных, например, при попытке создать ресурс с уже существующим именем. + **Использование**: Возвращает HTTP-статус `409 Conflict`. + **Пример**: Попытка создать эпик с названием, которое уже используется. + +- **`InvalidTaskDataException.java`** + **Описание**: Исключение для случаев, когда данные задачи, эпика или подзадачи некорректны (например, логические ошибки в данных). + **Использование**: Возвращает HTTP-статус `400 Bad Request`. + **Пример**: Указана некорректная длительность задачи. + +- **`NotFoundException.java`** + **Описание**: Исключение, выбрасываемое, когда запрашиваемый ресурс (задача, эпик, подзадача) не найден. + **Использование**: Возвращает HTTP-статус `404 Not Found`. + **Пример**: Запрос эпика по несуществующему ID. + +- **`TaskConstraintViolationException.java`** + **Описание**: Исключение для случаев, когда нарушены ограничения целостности данных задачи (например, бизнес-правила). + **Использование**: Возвращает HTTP-статус `400 Bad Request`. + **Пример**: Попытка создать подзадачу без указания связанного эпика. + +### 2. Обработчик ошибок + +- **`ErrorHandler.java`** + **Описание**: Централизованный обработчик исключений, реализованный с использованием `@RestControllerAdvice`. Преобразует исключения в HTTP-ответы с соответствующими статусами и сообщениями об ошибках. + **Обработанные исключения**: + - `NotFoundException` → `404 Not Found`, возвращает `ErrorResponse` с сообщением. + - `InvalidTaskDataException` → `400 Bad Request`, возвращает `ErrorResponse` с сообщением. + - `TaskConstraintViolationException` → `400 Bad Request`, возвращает `ErrorResponse` с сообщением. + - `ConflictException` → `409 Conflict`, возвращает `ErrorResponse` с сообщением. + - `MethodArgumentNotValidException` → `400 Bad Request`, возвращает `Map` с ошибками валидации полей DTO. + - `ConstraintViolationException` → `400 Bad Request`, возвращает `Map` с ошибками валидации (например, для `@PathVariable`). + - `HandlerMethodValidationException` → `400 Bad Request`, возвращает `Map` с ошибками валидации параметров метода. + **Внутренний рекорд**: + - `ErrorResponse` — Простая структура для возврата сообщения об ошибке в формате JSON: `{ "message": "Ошибка" }`. + +## Основные особенности + +- **Централизованная обработка**: `ErrorHandler` обрабатывает все исключения в одном месте, обеспечивая единообразие ответов. +- **Документация ошибок**: HTTP-статусы и сообщения об ошибках соответствуют стандартам REST API и документируются в Swagger (см. `/swagger-ui.html`). +- **Валидация**: + - Ошибки валидации DTO (`MethodArgumentNotValidException`) возвращают словарь с названиями полей и сообщениями об ошибках. + - Ошибки валидации параметров (`ConstraintViolationException`, `HandlerMethodValidationException`) также возвращают словарь с указанием проблемных параметров. +- **Гибкость**: Исключения покрывают различные сценарии (конфликты, отсутствие данных, некорректные запросы), что упрощает расширение системы. + +## Использование + +Исключения выбрасываются в сервисах или контроллерах, когда возникают ошибки. `ErrorHandler` автоматически перехватывает их и формирует ответ для клиента. + +**Пример ответа для `NotFoundException`**: +```json +{ + "message": "Эпик с ID 1 не найден" +} +``` + +**Пример ответа для `MethodArgumentNotValidException`**: +```json +{ + "name": "blank name", + "description": "blank description" +} +``` + +## Документация API + +Ошибки документируются в Swagger UI, доступном по пути: +- **Swagger UI**: `http://localhost:8080/swagger-ui.html` +- **OpenAPI JSON**: `http://localhost:8080/v3/api-docs` + +Каждый эндпоинт в контроллерах (`controller`) включает описание возможных ошибок и соответствующих HTTP-статусов (например, `404`, `409`). + +## Примечания + +- **Расширяемость**: Новые исключения можно добавить, создав класс, унаследованный от `RuntimeException`, и добавив его обработку в `ErrorHandler`. +- **Логирование**: Рекомендуется добавить логирование ошибок в `ErrorHandler` для отладки (например, с использованием SLF4J). +- **Консистентность**: Все ответы об ошибках возвращаются в формате JSON, что упрощает их обработку на клиентской стороне. + +## Дополнительно + +Для информации о контроллерах, DTO или сервисах см. соответствующие папки (`controller`, `dto`, `service`). По вопросам настройки или расширения системы обработки ошибок обратитесь к документации проекта или разработчикам. \ No newline at end of file diff --git a/service/src/main/java/service/task/manager/exception/TaskConstraintViolationException.java b/service/src/main/java/service/task/manager/exception/TaskConstraintViolationException.java new file mode 100644 index 0000000..7589e37 --- /dev/null +++ b/service/src/main/java/service/task/manager/exception/TaskConstraintViolationException.java @@ -0,0 +1,7 @@ +package service.task.manager.exception; + +public class TaskConstraintViolationException extends RuntimeException { + public TaskConstraintViolationException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/service/src/main/java/service/task/manager/mapper/EpicMapper.java b/service/src/main/java/service/task/manager/mapper/EpicMapper.java new file mode 100644 index 0000000..742ace8 --- /dev/null +++ b/service/src/main/java/service/task/manager/mapper/EpicMapper.java @@ -0,0 +1,42 @@ +package service.task.manager.mapper; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingConstants; +import org.mapstruct.MappingTarget; +import service.task.manager.dto.epic.EpicRequestCreatedDto; +import service.task.manager.dto.epic.EpicRequestUpdatedDto; +import service.task.manager.dto.epic.EpicResponseDto; +import service.task.manager.dto.subtask.SubtaskRequestUpdatedDto; +import service.task.manager.model.Epic; +import service.task.manager.model.Subtask; + +@Mapper(componentModel = MappingConstants.ComponentModel.SPRING) +public interface EpicMapper { + + // Маппинг из DTO в сущность Epic + Epic toEntity(EpicRequestCreatedDto epicRequestCreatedDto); + + // Маппинг из сущности Epic в DTO ответа + EpicResponseDto toResponseDto(Epic epic); + + // Маппинг для подзадач (Subtask -> SubtaskDto) + EpicResponseDto.SubtaskDto toSubtaskDto(Subtask subtask); + + Subtask toEntity(SubtaskRequestUpdatedDto subtaskRequestUpdatedDto); + + SubtaskRequestUpdatedDto toSubtaskRequestUpdatedDto(Subtask subtask); + + Epic toEntity(EpicRequestUpdatedDto epicRequestUpdatedDto); + + Epic toEntity(EpicResponseDto dto); + + EpicRequestUpdatedDto toEpicDto(Epic epic); + + @Mapping(target = "id", ignore = true) // Не обновляем ID + @Mapping(target = "startTime", ignore = true) // Оставляем startTime из базы + @Mapping(target = "subtasks", ignore = true) + @Mapping(target = "endTime", ignore = true) + // endTime рассчитывается в @PreUpdate + void updateTaskFromDto(EpicRequestUpdatedDto dto, @MappingTarget Epic epic); +} diff --git a/service/src/main/java/service/task/manager/mapper/README.md b/service/src/main/java/service/task/manager/mapper/README.md new file mode 100644 index 0000000..408f977 --- /dev/null +++ b/service/src/main/java/service/task/manager/mapper/README.md @@ -0,0 +1,96 @@ +# README.md для папки `mapper` + +## Описание + +Папка `mapper` содержит интерфейсы мапперов, реализованные с использованием библиотеки MapStruct для приложения `service.task.manager`. Мапперы обеспечивают преобразование между объектами передачи данных (DTO) и сущностями базы данных (`Task`, `Epic`, `Subtask`), а также между различными типами DTO. Это позволяет отделить логику преобразования данных от сервисного слоя, упрощая код и повышая его читаемость. Все мапперы интегрированы со Spring и используют аннотации для точной настройки маппинга. + +## Структура папки + +Папка включает следующие интерфейсы мапперов: + +### 1. `EpicMapper.java` +**Описание**: Интерфейс для преобразования данных, связанных с эпиками (`Epic`) и их подзадачами (`Subtask`). +**Методы**: +- `toEntity(EpicRequestCreatedDto) → Epic` — Преобразует DTO для создания эпика в сущность. +- `toResponseDto(Epic) → EpicResponseDto` — Преобразует сущность эпика в DTO ответа (включает подзадачи). +- `toSubtaskDto(Subtask) → EpicResponseDto.SubtaskDto` — Преобразует подзадачу в DTO для включения в ответ эпика. +- `toEntity(SubtaskRequestUpdatedDto) → Subtask` — Преобразует DTO обновления подзадачи в сущность. +- `toSubtaskRequestUpdatedDto(Subtask) → SubtaskRequestUpdatedDto` — Преобразует подзадачу в DTO обновления. +- `toEntity(EpicRequestUpdatedDto) → Epic` — Преобразует DTO обновления эпика в сущность. +- `toEntity(EpicResponseDto) → Epic` — Преобразует DTO ответа в сущность. +- `toEpicDto(Epic) → EpicRequestUpdatedDto` — Преобразует сущность эпика в DTO обновления. +- `updateTaskFromDto(EpicRequestUpdatedDto, @MappingTarget Epic)` — Обновляет сущность эпика на основе DTO, игнорируя поля `id`, `startTime`, `subtasks`, `endTime` (последнее рассчитывается в `@PreUpdate`). + +**Особенности**: +- Игнорирует `endTime`, так как оно вычисляется автоматически. +- Поддерживает вложенный маппинг подзадач для `EpicResponseDto`. + +### 2. `SubtaskMapper.java` +**Описание**: Интерфейс для преобразования данных, связанных с подзадачами (`Subtask`). +**Методы**: +- `toEntity(SubtaskRequestCreatedDto) → Subtask` — Преобразует DTO для создания подзадачи в сущность. +- `toResponseDto(Subtask) → SubtaskResponseDto` — Преобразует сущность подзадачи в DTO ответа. +- `toEntity(SubtaskRequestUpdatedDto) → Subtask` — Преобразует DTO обновления подзадачи в сущность. +- `toEntity(SubtaskResponseDto) → Subtask` — Преобразует DTO ответа в сущность. +- `updateSubtaskFromDto(SubtaskRequestUpdatedDto, @MappingTarget Subtask)` — Обновляет сущность подзадачи на основе DTO, игнорируя поля `id`, `epic`, `startTime`, `endTime` (последнее рассчитывается в `@PreUpdate`). + +**Особенности**: +- Игнорирует поле `epic`, чтобы сохранить связь с эпикой из базы данных. +- Поддерживает обновление подзадачи без изменения неизменяемых полей. + +### 3. `TaskMapper.java` +**Описание**: Интерфейс для преобразования данных, связанных с задачами (`Task`). +**Методы**: +- `toEntity(TaskRequestCreatedDto) → Task` — Преобразует DTO для создания задачи в сущность. +- `toResponseDto(Task) → TaskResponseDto` — Преобразует сущность задачи в DTO ответа. +- `toEntity(TaskRequestUpdatedDto) → Task` — Преобразует DTO обновления задачи в сущность. +- `toEntity(TaskResponseDto) → Task` — Преобразует DTO ответа в сущность. +- `toTaskRequestUpdatedDto(Task) → TaskRequestUpdatedDto` — Преобразует сущность задачи в DTO обновления. +- `updateTaskFromDto(TaskRequestUpdatedDto, @MappingTarget Task)` — Обновляет сущность задачи на основе DTO, игнорируя поля `id`, `startTime`, `endTime` (последнее рассчитывается в `@PreUpdate`). + +**Особенности**: +- Игнорирует `startTime` и `endTime` при обновлении, чтобы сохранить значения из базы. + +## Основные особенности + +- **MapStruct**: Все мапперы используют библиотеку MapStruct, которая генерирует реализацию маппинга во время компиляции, обеспечивая высокую производительность и типобезопасность. +- **Интеграция со Spring**: Аннотация `@Mapper(componentModel = "spring")` позволяет инжектировать мапперы как Spring-бины. +- **Аннотации `@Mapping`**: + - Используются для точной настройки маппинга, например, для игнорирования полей (`id`, `startTime`, `endTime`, `subtasks`, `epic`). + - Обеспечивают корректное преобразование сложных структур, таких как список подзадач в `EpicResponseDto`. +- **Обновление сущностей**: Методы `update...FromDto` используют `@MappingTarget` для частичного обновления сущностей, сохраняя неизменяемые поля. +- **Документация**: Мапперы косвенно документируются через DTO, которые содержат аннотации Swagger (`@Schema`). + +## Использование + +Мапперы используются в сервисах (`service`) для преобразования данных между DTO (из контроллеров) и сущностями (для работы с базой данных). Они упрощают обработку запросов, минимизируя ручной код преобразования. + +**Пример использования в сервисе**: +```java +Epic epic = epicMapper.toEntity(epicRequestCreatedDto); +epicRepository.save(epic); +EpicResponseDto response = epicMapper.toResponseDto(epic); +``` + +## Зависимости + +- **MapStruct**: Библиотека для генерации мапперов. +- **DTO**: Пакет `service.task.manager.dto` (папки `task`, `epic`, `subtask`). +- **Модели**: Пакет `service.task.manager.model` (классы `Task`, `Epic`, `Subtask`). +- **Spring**: Для интеграции мапперов как бинов. + +## Документация API + +Мапперы не имеют прямой документации в Swagger, но их использование отражено в DTO, которые описаны в Swagger UI: +- **Swagger UI**: `http://localhost:8080/swagger-ui.html` +- **OpenAPI JSON**: `http://localhost:8080/v3/api-docs` + +## Примечания + +- **Игнорируемые поля**: Поля, такие как `id`, `startTime`, `endTime`, игнорируются при обновлении, чтобы избежать нежелательных изменений. `endTime` рассчитывается автоматически в сущностях с помощью аннотации `@PreUpdate`. +- **Производительность**: MapStruct генерирует эффективный код, минимизируя накладные расходы на преобразование. +- **Расширяемость**: Для добавления новых маппингов достаточно расширить существующие интерфейсы или создать новые. + +## Дополнительно + +Для информации о DTO, контроллерах или сервисах см. соответствующие папки (`dto`, `controller`, `service`). По вопросам настройки или расширения мапперов обратитесь к документации проекта или разработчикам. \ No newline at end of file diff --git a/service/src/main/java/service/task/manager/mapper/SubtaskMapper.java b/service/src/main/java/service/task/manager/mapper/SubtaskMapper.java new file mode 100644 index 0000000..062a2fe --- /dev/null +++ b/service/src/main/java/service/task/manager/mapper/SubtaskMapper.java @@ -0,0 +1,31 @@ +package service.task.manager.mapper; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingConstants; +import org.mapstruct.MappingTarget; +import service.task.manager.dto.subtask.SubtaskRequestCreatedDto; +import service.task.manager.dto.subtask.SubtaskRequestUpdatedDto; +import service.task.manager.dto.subtask.SubtaskResponseDto; +import service.task.manager.model.Subtask; + +@Mapper(componentModel = MappingConstants.ComponentModel.SPRING) +public interface SubtaskMapper { + + // Маппинг из DTO в сущность Subtask + Subtask toEntity(SubtaskRequestCreatedDto dto); + + // Маппинг из сущности Subtask в DTO ответа + SubtaskResponseDto toResponseDto(Subtask subtask); + + Subtask toEntity(SubtaskRequestUpdatedDto dto); + + Subtask toEntity(SubtaskResponseDto dto); + + @Mapping(target = "id", ignore = true) // Не обновляем ID + @Mapping(target = "epic", ignore = true) + @Mapping(target = "startTime", ignore = true) // Оставляем startTime из базы + @Mapping(target = "endTime", ignore = true) + // endTime рассчитывается в @PreUpdate + void updateSubtaskFromDto(SubtaskRequestUpdatedDto dto, @MappingTarget Subtask subtask); +} diff --git a/service/src/main/java/service/task/manager/mapper/TaskMapper.java b/service/src/main/java/service/task/manager/mapper/TaskMapper.java new file mode 100644 index 0000000..c6062b4 --- /dev/null +++ b/service/src/main/java/service/task/manager/mapper/TaskMapper.java @@ -0,0 +1,32 @@ +package service.task.manager.mapper; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; +import service.task.manager.dto.task.TaskRequestCreatedDto; +import service.task.manager.dto.task.TaskRequestUpdatedDto; +import service.task.manager.dto.task.TaskResponseDto; +import service.task.manager.model.Task; + +@Mapper(componentModel = "spring") +public interface TaskMapper { + + // Маппинг из DTO в сущность Task + Task toEntity(TaskRequestCreatedDto taskRequestCreatedDto); + + // Маппинг из сущности Task в DTO ответа + TaskResponseDto toResponseDto(Task task); + + Task toEntity(TaskRequestUpdatedDto taskRequestUpdatedDto); + + Task toEntity(TaskResponseDto dto); + + TaskRequestUpdatedDto toTaskRequestUpdatedDto(Task task); + + // Метод для обновления существующей сущности + @Mapping(target = "id", ignore = true) // Не обновляем ID + @Mapping(target = "startTime", ignore = true) // Оставляем startTime из базы + @Mapping(target = "endTime", ignore = true) + // endTime рассчитывается в @PreUpdate + void updateTaskFromDto(TaskRequestUpdatedDto dto, @MappingTarget Task task); +} diff --git a/service/src/main/java/service/task/manager/model/Epic.java b/service/src/main/java/service/task/manager/model/Epic.java new file mode 100644 index 0000000..33aac29 --- /dev/null +++ b/service/src/main/java/service/task/manager/model/Epic.java @@ -0,0 +1,70 @@ +package service.task.manager.model; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import org.hibernate.proxy.HibernateProxy; +import service.task.manager.model.enums.Status; +import service.task.manager.model.enums.TaskType; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +@Getter +@ToString(exclude = "subtasks") +@Setter +@Entity +@Table(name = "epic") +public class Epic { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @OneToMany(mappedBy = "epic", cascade = CascadeType.ALL, orphanRemoval = true) + private List subtasks = new ArrayList<>(); + + @Column(nullable = false) + private String name; + + private String description; + + @Enumerated(EnumType.STRING) + private Status status; + + @Column(name = "start_time") + private LocalDateTime startTime; + + private Duration duration; + + @Column(name = "end_time") + private LocalDateTime endTime; + + @Setter(AccessLevel.NONE) + @Enumerated(EnumType.STRING) + private TaskType type = TaskType.EPIC; + + @PrePersist + @PreUpdate + public void calculateEndTime() { + if (startTime != null && duration != null) { + endTime = startTime.plus(duration); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Epic epic)) return false; + return Objects.equals(id, epic.id); + } + + @Override + public int hashCode() { + return Objects.hashCode(id); + } +} \ No newline at end of file diff --git a/service/src/main/java/service/task/manager/model/HistoryEntry.java b/service/src/main/java/service/task/manager/model/HistoryEntry.java new file mode 100644 index 0000000..c0f6bf4 --- /dev/null +++ b/service/src/main/java/service/task/manager/model/HistoryEntry.java @@ -0,0 +1,14 @@ +package service.task.manager.model; + +import lombok.*; +import service.task.manager.model.enums.TaskType; + +@AllArgsConstructor +@NoArgsConstructor +@Getter +@Setter +@ToString +public class HistoryEntry { + private TaskType type; + private Long id; +} diff --git a/service/src/main/java/service/task/manager/model/README.md b/service/src/main/java/service/task/manager/model/README.md new file mode 100644 index 0000000..10d0055 --- /dev/null +++ b/service/src/main/java/service/task/manager/model/README.md @@ -0,0 +1,136 @@ +# README.md для папки `model` + +## Описание + +Папка `model` содержит сущности (модели) и перечисления, используемые для представления данных в приложении `service.task.manager`. Модели описывают структуру данных для задач, эпиков, подзадач и записей истории, которые хранятся в базе данных. Папка также включает подпапку `enums`, содержащую перечисления для статусов и типов задач. Сущности аннотированы с использованием JPA (Jakarta Persistence API) для работы с реляционной базой данных через Hibernate. + +## Структура папки + +### 1. Модели + +- **`Epic.java`** + **Описание**: Сущность, представляющая эпик — крупную задачу, которая может содержать подзадачи. + **Поля**: + - `id` (`Long`, автоинкремент) — Уникальный идентификатор эпика. + - `subtasks` (`List`) — Список связанных подзадач (связь `@OneToMany`). + - `name` (`String`, обязательное) — Название эпика. + - `description` (`String`) — Описание эпика. + - `status` (`Status`) — Статус эпика (`NEW`, `IN_PROGRESS`, `DONE`). + - `startTime` (`LocalDateTime`) — Время начала эпика. + - `duration` (`Duration`) — Длительность эпика. + - `endTime` (`LocalDateTime`) — Время окончания, рассчитывается автоматически. + - `type` (`TaskType`, только для чтения) — Тип задачи (всегда `EPIC`). + **Особенности**: + - Аннотация `@PrePersist` и `@PreUpdate` для автоматического расчета `endTime` на основе `startTime` и `duration`. + - Связь с подзадачами настроена с каскадным удалением (`cascade = CascadeType.ALL`, `orphanRemoval = true`). + +- **`HistoryEntry.java`** + **Описание**: Класс для представления записи в истории доступа к задачам, эпикам или подзадачам. + **Поля**: + - `type` (`TaskType`) — Тип сущности (`TASK`, `SUBTASK`, `EPIC`). + - `id` (`Long`) — Идентификатор сущности. + **Особенности**: + - Не является JPA-сущностью, используется для передачи данных о вызове метода `findById` (хранится в Redis). + +- **`Subtask.java`** + **Описание**: Сущность, представляющая подзадачу, связанную с эпикой. + **Поля**: + - `id` (`Long`, автоинкремент) — Уникальный идентификатор подзадачи. + - `epic` (`Epic`) — Связанный эпик (связь `@ManyToOne`). + - `name` (`String`, обязательное) — Название подзадачи. + - `description` (`String`) — Описание подзадачи. + - `status` (`Status`) — Статус подзадачи. + - `startTime` (`LocalDateTime`) — Время начала. + - `endTime` (`LocalDateTime`) — Время окончания, рассчитывается автоматически. + - `duration` (`Duration`) — Длительность подзадачи. + - `type` (`TaskType`, только для чтения) — Тип задачи (всегда `SUBTASK`). + **Особенности**: + - Автоматический расчет `endTime` через `@PrePersist` и `@PreUpdate`. + - Связь с эпикой через внешний ключ (`epic_id`). + +- **`Task.java`** + **Описание**: Сущность, представляющая независимую задачу. + **Поля**: + - `id` (`Long`, автоинкремент) — Уникальный идентификатор задачи. + - `name` (`String`, обязательное) — Название задачи. + - `description` (`String`) — Описание задачи. + - `status` (`Status`) — Статус задачи. + - `startTime` (`LocalDateTime`) — Время начала. + - `endTime` (`LocalDateTime`) — Время окончания, рассчитывается автоматически. + - `duration` (`Duration`) — Длительность задачи. + - `type` (`TaskType`, только для чтения) — Тип задачи (всегда `TASK`). + **Особенности**: + - Автоматический расчет `endTime` через `@PrePersist` и `@PreUpdate`. + +### 2. Папка `enums` + +- **`Status.java`** + **Описание**: Перечисление, определяющее возможные статусы задач, эпиков и подзадач. + **Значения**: + - `NEW` — Новая задача/эпик/подзадача. + - `IN_PROGRESS` — В процессе выполнения. + - `DONE` — Завершена. + **Использование**: Хранится в базе данных как строка (`@Enumerated(EnumType.STRING)`). + +- **`TaskType.java`** + **Описание**: Перечисление, определяющее тип сущности. + **Значения**: + - `TASK` — Независимая задача. + - `SUBTASK` — Подзадача, связанная с эпикой. + - `EPIC` — Эпик, содержащий подзадачи. + **Использование**: Используется для идентификации типа сущности в базе данных и в истории доступа. + +## Основные особенности + +- **JPA-аннотации**: + - `@Entity` и `@Table` для маппинга сущностей на таблицы базы данных (`task`, `epic`, `subtask`). + - `@Id` и `@GeneratedValue` для автоинкрементных идентификаторов. + - `@OneToMany` и `@ManyToOne` для связей между эпиками и подзадачами. + - `@Enumerated(EnumType.STRING)` для хранения перечислений как строк. +- **Автоматический расчет `endTime`**: Все сущности (`Task`, `Epic`, `Subtask`) используют метод `calculateEndTime`, вызываемый перед сохранением или обновлением, для вычисления `endTime` на основе `startTime` и `duration`. +- **Lombok**: Аннотации `@Getter`, `@Setter`, `@ToString`, `@RequiredArgsConstructor` и др. используются для упрощения кода. +- **Равенство и хэш-код**: Реализованы методы `equals` и `hashCode`, основанные на поле `id`, для корректной работы с коллекциями и Hibernate. +- **Исключение циклических ссылок**: В `@ToString` для `Epic` исключено поле `subtasks`, чтобы избежать рекурсии при выводе. + +## Использование + +Модели используются в сервисах и репозиториях для работы с базой данных. Они представляют данные, хранимые в таблицах, и используются мапперами (`mapper`) для преобразования в DTO и обратно. + +**Пример использования в сервисе**: +```java +Epic epic = new Epic(); +epic.setName("Новый эпик"); +epic.setStartTime(LocalDateTime.now()); +epic.setDuration(Duration.ofHours(24)); +epicRepository.save(epic); // endTime рассчитается автоматически +``` + +**Пример записи в историю**: +```java +HistoryEntry entry = new HistoryEntry(TaskType.EPIC, epic.getId()); +historyService.save(entry); +``` + +## Зависимости + +- **Hibernate/JPA**: Для работы с базой данных. +- **Lombok**: Для генерации геттеров, сеттеров и других методов. +- **DTO**: Пакет `service.task.manager.dto` для преобразования моделей в DTO через мапперы. +- **MapStruct**: Пакет `service.task.manager.mapper` для маппинга. + +## Документация API + +Модели не документируются напрямую в Swagger, но их структура отражена в DTO, которые описаны в Swagger UI: +- **Swagger UI**: `http://localhost:8080/swagger-ui.html` +- **OpenAPI JSON**: `http://localhost:8080/v3/api-docs` + +## Примечания + +- **Автоматический расчет `endTime`**: Поле `endTime` не должно устанавливаться вручную, так как оно вычисляется автоматически. +- **Связи**: Подзадачи автоматически удаляются при удалении эпика благодаря настройке `orphanRemoval = true`. +- **Перечисления**: `Status` и `TaskType` хранятся как строки в базе данных для удобства чтения и расширения. +- **История**: `HistoryEntry` используется только для кэширования в Redis, а не для хранения в реляционной базе данных. + +## Дополнительно + +Для информации о мапперах, DTO, контроллерах или сервисах см. соответствующие папки (`mapper`, `dto`, `controller`, `service`). По вопросам настройки или расширения моделей обратитесь к документации проекта или разработчикам. \ No newline at end of file diff --git a/service/src/main/java/service/task/manager/model/Subtask.java b/service/src/main/java/service/task/manager/model/Subtask.java new file mode 100644 index 0000000..b202776 --- /dev/null +++ b/service/src/main/java/service/task/manager/model/Subtask.java @@ -0,0 +1,67 @@ +package service.task.manager.model; + +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.proxy.HibernateProxy; +import service.task.manager.model.enums.Status; +import service.task.manager.model.enums.TaskType; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.Objects; + +@Getter +@Setter +@ToString +@RequiredArgsConstructor +@Entity +@Table(name = "subtask") +public class Subtask { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "epic_id") + private Epic epic; + + @Column(nullable = false) + private String name; + + private String description; + + @Enumerated(EnumType.STRING) + private Status status; + + @Column(name = "start_time") + private LocalDateTime startTime; + + @Column(name = "end_time") + private LocalDateTime endTime; + + private Duration duration; + + @Setter(AccessLevel.NONE) + @Enumerated(EnumType.STRING) + private TaskType type = TaskType.SUBTASK; + + @PrePersist + @PreUpdate + public void calculateEndTime() { + if (startTime != null && duration != null) { + endTime = startTime.plus(duration); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Subtask subtask)) return false; + return Objects.equals(id, subtask.id); + } + + @Override + public int hashCode() { + return Objects.hashCode(id); + } +} \ No newline at end of file diff --git a/service/src/main/java/service/task/manager/model/Task.java b/service/src/main/java/service/task/manager/model/Task.java new file mode 100644 index 0000000..f133ea3 --- /dev/null +++ b/service/src/main/java/service/task/manager/model/Task.java @@ -0,0 +1,65 @@ +package service.task.manager.model; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import org.hibernate.proxy.HibernateProxy; +import service.task.manager.model.enums.Status; +import service.task.manager.model.enums.TaskType; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.Objects; + +@Getter +@Setter +@ToString +@Entity +@Table(name = "task") +public class Task { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String name; + + private String description; + + @Enumerated(EnumType.STRING) + private Status status; + + @Column(name = "start_time") + private LocalDateTime startTime; + + @Column(name = "end_time") + private LocalDateTime endTime; + + private Duration duration; + + @Setter(AccessLevel.NONE) + @Enumerated(EnumType.STRING) + private TaskType type = TaskType.TASK; + + @PrePersist + @PreUpdate + public void calculateEndTime() { + if (startTime != null && duration != null) { + endTime = startTime.plus(duration); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Task task)) return false; + return Objects.equals(id, task.id); + } + + @Override + public int hashCode() { + return Objects.hashCode(id); + } +} \ No newline at end of file diff --git a/src/task/manager/schedule/model/Status.java b/service/src/main/java/service/task/manager/model/enums/Status.java similarity index 58% rename from src/task/manager/schedule/model/Status.java rename to service/src/main/java/service/task/manager/model/enums/Status.java index daf6206..19818b4 100644 --- a/src/task/manager/schedule/model/Status.java +++ b/service/src/main/java/service/task/manager/model/enums/Status.java @@ -1,4 +1,4 @@ -package task.manager.schedule.model; +package service.task.manager.model.enums; public enum Status { NEW, diff --git a/src/task/manager/schedule/model/TaskType.java b/service/src/main/java/service/task/manager/model/enums/TaskType.java similarity index 58% rename from src/task/manager/schedule/model/TaskType.java rename to service/src/main/java/service/task/manager/model/enums/TaskType.java index 8b745a0..3866070 100644 --- a/src/task/manager/schedule/model/TaskType.java +++ b/service/src/main/java/service/task/manager/model/enums/TaskType.java @@ -1,4 +1,4 @@ -package task.manager.schedule.model; +package service.task.manager.model.enums; public enum TaskType { TASK, diff --git a/service/src/main/java/service/task/manager/repository/EpicRepository.java b/service/src/main/java/service/task/manager/repository/EpicRepository.java new file mode 100644 index 0000000..21f0e52 --- /dev/null +++ b/service/src/main/java/service/task/manager/repository/EpicRepository.java @@ -0,0 +1,10 @@ +package service.task.manager.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import service.task.manager.model.Epic; + +@Repository +public interface EpicRepository extends JpaRepository { + boolean existsByName(String name); +} \ No newline at end of file diff --git a/service/src/main/java/service/task/manager/repository/README.md b/service/src/main/java/service/task/manager/repository/README.md new file mode 100644 index 0000000..813d859 --- /dev/null +++ b/service/src/main/java/service/task/manager/repository/README.md @@ -0,0 +1,86 @@ +# README.md для папки `repository` + +## Описание + +Папка `repository` содержит интерфейсы репозиториев для работы с базой данных в приложении `service.task.manager`. Репозитории реализованы с использованием Spring Data JPA и предоставляют методы для выполнения CRUD-операций (создание, чтение, обновление, удаление) над сущностями `Task`, `Epic` и `Subtask`. Каждый репозиторий расширяет интерфейс `JpaRepository`, что обеспечивает стандартные методы для работы с данными, а также включает дополнительные методы для специфичных проверок. + +## Структура папки + +### 1. `EpicRepository.java` +**Описание**: Интерфейс репозитория для работы с сущностью `Epic` (эпики). +**Методы**: +- Наследуемые от `JpaRepository`: + - `save`, `findById`, `findAll`, `deleteById`, и др. — стандартные CRUD-операции. +- Специфичный метод: + - `boolean existsByName(String name)` — Проверяет, существует ли эпик с указанным именем. + **Использование**: Используется для создания, получения, обновления и удаления эпиков, а также для проверки уникальности имени эпика. + +### 2. `SubtaskRepository.java` +**Описание**: Интерфейс репозитория для работы с сущностью `Subtask` (подзадачи). +**Методы**: +- Наследуемые от `JpaRepository`: + - `save`, `findById`, `findAll`, `deleteById`, и др. — стандартные CRUD-операции. +- Специфичный метод: + - `boolean existsByName(String name)` — Проверяет, существует ли подзадача с указанным именем. + **Использование**: Используется для управления подзадачами, связанными с эпиками, и проверки уникальности имени подзадачи. + +### 3. `TaskRepository.java` +**Описание**: Интерфейс репозитория для работы с сущностью `Task` (задачи). +**Методы**: +- Наследуемые от `JpaRepository`: + - `save`, `findById`, `findAll`, `deleteById`, и др. — стандартные CRUD-операции. +- Специфичный метод: + - `boolean existsByName(String name)` — Проверяет, существует ли задача с указанным именем. + **Использование**: Используется для управления независимыми задачами и проверки уникальности имени задачи. + +## Основные особенности + +- **Spring Data JPA**: Репозитории используют Spring Data JPA для автоматической генерации запросов к базе данных на основе имен методов и аннотаций. +- **Аннотация `@Repository`**: Помечает интерфейсы как Spring-бины, интегрируемые с контекстом приложения. +- **Уникальность имен**: Метод `existsByName` в каждом репозитории позволяет проверять уникальность имени сущности, что предотвращает создание дубликатов (например, используется для генерации исключения `ConflictException`). +- **Типизация**: Репозитории строго типизированы для работы с соответствующими сущностями (`Epic`, `Subtask`, `Task`) и их идентификаторами (`Long`). +- **Каскадные операции**: Для `EpicRepository` операции над эпиками автоматически применяются к связанным подзадачам благодаря настройке `cascade = CascadeType.ALL` в модели `Epic`. + +## Использование + +Репозитории используются в сервисах (`service`) для взаимодействия с базой данных. Они предоставляют удобный интерфейс для выполнения операций без необходимости написания SQL-запросов. + +**Пример использования в сервисе**: +```java +@Service +@RequiredArgsConstructor +public class EpicService { + private final EpicRepository epicRepository; + + public void create(Epic epic) { + if (epicRepository.existsByName(epic.getName())) { + throw new ConflictException("Эпик с именем " + epic.getName() + " уже существует"); + } + epicRepository.save(epic); + } +} +``` + +## Зависимости + +- **Spring Data JPA**: Для реализации репозиториев и взаимодействия с базой данных. +- **Hibernate**: Как реализация JPA для маппинга сущностей на таблицы. +- **Модели**: Пакет `service.task.manager.model` (классы `Task`, `Epic`, `Subtask`). +- **Исключения**: Пакет `service.task.manager.exception` (например, `ConflictException` для обработки ошибок). + +## Документация API + +Репозитории не документируются напрямую в Swagger, но их функциональность отражена в эндпоинтах контроллеров, которые используют сервисы и, соответственно, репозитории. Документация доступна по: +- **Swagger UI**: `http://localhost:8080/swagger-ui.html` +- **OpenAPI JSON**: `http://localhost:8080/v3/api-docs` + +## Примечания + +- **Автоматические методы**: `JpaRepository` предоставляет множество встроенных методов, таких как `findAll`, `save`, `delete`, что минимизирует необходимость написания пользовательских запросов. +- **Проверка уникальности**: Метод `existsByName` используется для предотвращения дублирования имен, что является частью бизнес-логики (например, валидация перед сохранением). +- **Производительность**: Spring Data JPA оптимизирует запросы, но для сложных операций рекомендуется использовать `@Query` или другие механизмы, если потребуется. +- **Расширяемость**: Для добавления новых методов достаточно расширить интерфейсы репозиториев, используя соглашения об именовании Spring Data или аннотацию `@Query`. + +## Дополнительно + +Для информации о моделях, сервисах, мапперах или контроллерах см. соответствующие папки (`model`, `service`, `mapper`, `controller`). По вопросам настройки или расширения репозиториев обратитесь к документации проекта или разработчикам. \ No newline at end of file diff --git a/service/src/main/java/service/task/manager/repository/SubtaskRepository.java b/service/src/main/java/service/task/manager/repository/SubtaskRepository.java new file mode 100644 index 0000000..c59c6fb --- /dev/null +++ b/service/src/main/java/service/task/manager/repository/SubtaskRepository.java @@ -0,0 +1,10 @@ +package service.task.manager.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import service.task.manager.model.Subtask; + +@Repository +public interface SubtaskRepository extends JpaRepository { + boolean existsByName(String name); +} \ No newline at end of file diff --git a/service/src/main/java/service/task/manager/repository/TaskRepository.java b/service/src/main/java/service/task/manager/repository/TaskRepository.java new file mode 100644 index 0000000..5b63d35 --- /dev/null +++ b/service/src/main/java/service/task/manager/repository/TaskRepository.java @@ -0,0 +1,10 @@ +package service.task.manager.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import service.task.manager.model.Task; + +@Repository +public interface TaskRepository extends JpaRepository { + boolean existsByName(String name); +} \ No newline at end of file diff --git a/service/src/main/java/service/task/manager/service/EpicService.java b/service/src/main/java/service/task/manager/service/EpicService.java new file mode 100644 index 0000000..d96fcd5 --- /dev/null +++ b/service/src/main/java/service/task/manager/service/EpicService.java @@ -0,0 +1,21 @@ +package service.task.manager.service; + +import service.task.manager.dto.epic.EpicRequestCreatedDto; +import service.task.manager.dto.epic.EpicRequestUpdatedDto; +import service.task.manager.dto.epic.EpicResponseDto; + +import java.util.List; + +public interface EpicService { + void create(EpicRequestCreatedDto dto); + + EpicResponseDto update(EpicRequestUpdatedDto dto); + + EpicResponseDto findById(Long id); + + List findAll(); + + void delete(Long id); + + List prioritized(); +} diff --git a/service/src/main/java/service/task/manager/service/HistoryService.java b/service/src/main/java/service/task/manager/service/HistoryService.java new file mode 100644 index 0000000..beadd90 --- /dev/null +++ b/service/src/main/java/service/task/manager/service/HistoryService.java @@ -0,0 +1,13 @@ +package service.task.manager.service; + +import service.task.manager.model.HistoryEntry; +import service.task.manager.model.enums.TaskType; + +import java.util.List; + +public interface HistoryService { + + void addToHistory(TaskType type, Long id); + + List getHistory(); +} diff --git a/service/src/main/java/service/task/manager/service/README.md b/service/src/main/java/service/task/manager/service/README.md new file mode 100644 index 0000000..bfcf83a --- /dev/null +++ b/service/src/main/java/service/task/manager/service/README.md @@ -0,0 +1,147 @@ +# README.md для папки `service` + +## Описание + +Папка `service` содержит интерфейсы сервисов и их реализации для приложения `service.task.manager`. Сервисы реализуют бизнес-логику приложения, включая создание, обновление, получение и удаление задач, эпиков, подзадач, а также управление историей доступа. Интерфейсы определяют контракт для каждого сервиса, а их реализации находятся в подпапке `impl`. Все сервисы используют Spring-компоненты, транзакции, мапперы, репозитории и обработку исключений для обеспечения надежной работы. + +## Структура папки + +### 1. Интерфейсы сервисов + +- **`EpicService.java`** + **Описание**: Интерфейс для управления эпиками. + **Методы**: + - `create(EpicRequestCreatedDto)` — Создание нового эпика. + - `update(EpicRequestUpdatedDto) → EpicResponseDto` — Обновление существующего эпика. + - `findById(Long) → EpicResponseDto` — Получение эпика по ID. + - `findAll() → List` — Получение всех эпиков. + - `delete(Long)` — Удаление эпика по ID. + - `prioritized() → List` — Получение эпиков, отсортированных по приоритету (`IN_PROGRESS`, `NEW`, `DONE`) и времени завершения. + +- **`HistoryService.java`** + **Описание**: Интерфейс для управления историей доступа к задачам, эпикам и подзадачам. + **Методы**: + - `addToHistory(TaskType, Long)` — Добавление записи в историю (тип сущности и ID). + - `getHistory() → List` — Получение последних 10 записей истории. + +- **`SubtaskService.java`** + **Описание**: Интерфейс для управления подзадачами. + **Методы**: + - `create(SubtaskRequestCreatedDto)` — Создание новой подзадачи. + - `update(SubtaskRequestUpdatedDto) → SubtaskResponseDto` — Обновление существующей подзадачи. + - `findById(Long) → SubtaskResponseDto` — Получение подзадачи по ID. + - `findAll() → List` — Получение всех подзадач. + - `delete(Long)` — Удаление подзадачи по ID. + - `prioritized() → List` — Получение подзадач, отсортированных по приоритету и времени завершения. + +- **`TaskService.java`** + **Описание**: Интерфейс для управления задачами. + **Методы**: + - `create(TaskRequestCreatedDto)` — Создание новой задачи. + - `update(TaskRequestUpdatedDto) → TaskResponseDto` — Обновление существующей задачи. + - `findById(Long) → TaskResponseDto` — Получение задачи по ID. + - `findAll() → List` — Получение всех задач. + - `delete(Long)` — Удаление задачи по ID. + - `prioritized() → List` — Получение задач, отсортированных по приоритету и времени завершения. + +### 2. Папка `impl` (реализации сервисов) + +- **`EpicServiceImpl.java`** + **Описание**: Реализация `EpicService` для управления эпиками. + **Особенности**: + - Проверяет уникальность имени эпика с помощью `EpicRepository.existsByName`. + - Устанавливает статус `NEW` для новых эпиков и вызывает расчет `endTime`. + - Использует `EpicMapper` для преобразования между DTO и сущностями. + - Добавляет запись в историю при вызове `findById` через `HistoryService`. + - Реализует сортировку в методе `prioritized` по статусу (`IN_PROGRESS` → `NEW` → `DONE`) и `endTime`. + - Управляет транзакциями с помощью `@Transactional` (чтение — `readOnly = true`). + +- **`HistoryServiceImpl.java`** + **Описание**: Реализация `HistoryService` для хранения и получения истории доступа в Redis. + **Особенности**: + - Хранит до 10 записей в Redis (ключ `history`), удаляя старую запись при превышении лимита. + - Использует `RedisTemplate` и `ListOperations` для работы с Redis. + - Логирует ошибки и возвращает пустой список в случае сбоя. + - Не использует транзакции, так как работает с кэшем, а не с базой данных. + +- **`SubtaskServiceImpl.java`** + **Описание**: Реализация `SubtaskService` для управления подзадачами. + **Особенности**: + - Проверяет существование эпика через `EpicService.findById` перед созданием подзадачи. + - Проверяет уникальность имени подзадачи с помощью `SubtaskRepository.existsByName`. + - Устанавливает статус `NEW` и вызывает расчет `endTime` для новых подзадач. + - Использует `SubtaskMapper` и `EpicMapper` для преобразования данных. + - Добавляет запись в историю при вызове `findById`. + - Реализует сортировку в методе `prioritized` аналогично эпикам. + - Использует `@Transactional` для управления транзакциями. + +- **`TaskServiceImpl.java`** + **Описание**: Реализация `TaskService` для управления задачами. + **Особенности**: + - Проверяет уникальность имени задачи с помощью `TaskRepository.existsByName`. + - Устанавливает статус `NEW` и вызывает расчет `endTime` для новых задач. + - Использует `TaskMapper` для преобразования между DTO и сущностями. + - Добавляет запись в историю при вызове `findById`. + - Реализует сортировку в методе `prioritized` аналогично эпикам и подзадачам. + - Использует `@Transactional` для управления транзакциями. + +## Основные особенности + +- **Интерфейсы и реализации**: Разделение на интерфейсы и их реализации (`impl`) упрощает тестирование и замену реализаций. +- **Транзакции**: Используется аннотация `@Transactional` для обеспечения целостности данных; операции чтения помечены как `readOnly = true` для оптимизации. +- **Логирование**: Все сервисы используют SLF4J (`@Slf4j`) для логирования операций и ошибок. +- **Обработка ошибок**: + - `NotFoundException` для отсутствующих ресурсов. + - `ConflictException` для дублирования имен. +- **Сортировка по приоритету**: Метод `prioritized` сортирует сущности по статусу (`IN_PROGRESS` → `NEW` → `DONE`) и времени завершения (`endTime`). +- **Redis для истории**: `HistoryServiceImpl` использует Redis для хранения истории доступа, обеспечивая быстрый доступ и ограничение размера (10 записей). +- **Маппинг**: Все сервисы используют мапперы (`EpicMapper`, `SubtaskMapper`, `TaskMapper`) для преобразования между DTO и сущностями. + +## Использование + +Сервисы используются в контроллерах (`controller`) для обработки запросов и реализации бизнес-логики. Они взаимодействуют с репозиториями для доступа к базе данных и мапперами для преобразования данных. + +**Пример использования в контроллере**: +```java +@PostMapping +public ResponseEntity create(@RequestBody @Valid EpicRequestCreatedDto dto) { + epicService.create(dto); + return ResponseEntity.status(HttpStatus.CREATED).build(); +} +``` + +**Пример получения истории**: +```java +@GetMapping +public List getHistory() { + return historyService.getHistory(); +} +``` + +## Зависимости + +- **Spring**: Для управления сервисами как бинами (`@Service`) и транзакциями (`@Transactional`). +- **Spring Data JPA**: Для взаимодействия с репозиториями (`EpicRepository`, `SubtaskRepository`, `TaskRepository`). +- **MapStruct**: Для маппинга между DTO и сущностями (`EpicMapper`, `SubtaskMapper`, `TaskMapper`). +- **Redis**: Для хранения истории в `HistoryServiceImpl` через `RedisTemplate`. +- **Lombok**: Для упрощения кода (`@RequiredArgsConstructor`, `@Slf4j`). +- **Исключения**: Пакет `service.task.manager.exception` для обработки ошибок (`NotFoundException`, `ConflictException`). +- **Модели**: Пакет `service.task.manager.model` для работы с сущностями (`Task`, `Epic`, `Subtask`, `HistoryEntry`). + +## Документация API + +Сервисы косвенно документируются через контроллеры, которые используют их методы. Полная документация доступна в Swagger UI: +- **Swagger UI**: `http://localhost:8080/swagger-ui.html` +- **OpenAPI JSON**: `http://localhost:8080/v3/api-docs` + +## Примечания + +- **Транзакционная целостность**: Все операции, изменяющие данные, выполняются в транзакциях для обеспечения согласованности. +- **Логирование**: Подробное логирование операций и ошибок помогает в отладке и мониторинге. +- **Ограничение истории**: `HistoryServiceImpl` хранит только последние 10 записей, что оптимизирует использование памяти в Redis. +- **Сортировка**: Логика сортировки в методе `prioritized` унифицирована для всех сущностей, но может быть расширена для дополнительных критериев. +- **Расширяемость**: Новые сервисы можно добавить, создав интерфейс и реализацию, следуя существующей структуре. + +## Дополнительно + +Для информации о моделях, репозиториях, мапперах или контроллерах см. соответствующие папки (`model`, `repository`, `mapper`, `controller`). По вопросам настройки или расширения сервисов обратитесь к документации проекта или разработчикам. \ No newline at end of file diff --git a/service/src/main/java/service/task/manager/service/SubtaskService.java b/service/src/main/java/service/task/manager/service/SubtaskService.java new file mode 100644 index 0000000..d1d542f --- /dev/null +++ b/service/src/main/java/service/task/manager/service/SubtaskService.java @@ -0,0 +1,21 @@ +package service.task.manager.service; + +import service.task.manager.dto.subtask.SubtaskRequestCreatedDto; +import service.task.manager.dto.subtask.SubtaskRequestUpdatedDto; +import service.task.manager.dto.subtask.SubtaskResponseDto; + +import java.util.List; + +public interface SubtaskService { + void create(SubtaskRequestCreatedDto dto); + + SubtaskResponseDto update(SubtaskRequestUpdatedDto dto); + + SubtaskResponseDto findById(Long id); + + List findAll(); + + void delete(Long id); + + List prioritized(); +} diff --git a/service/src/main/java/service/task/manager/service/TaskService.java b/service/src/main/java/service/task/manager/service/TaskService.java new file mode 100644 index 0000000..198594e --- /dev/null +++ b/service/src/main/java/service/task/manager/service/TaskService.java @@ -0,0 +1,21 @@ +package service.task.manager.service; + +import service.task.manager.dto.task.TaskRequestCreatedDto; +import service.task.manager.dto.task.TaskRequestUpdatedDto; +import service.task.manager.dto.task.TaskResponseDto; + +import java.util.List; + +public interface TaskService { + void create(TaskRequestCreatedDto dto); + + TaskResponseDto update(TaskRequestUpdatedDto dto); + + TaskResponseDto findById(Long id); + + List findAll(); + + void delete(Long id); + + List prioritized(); +} diff --git a/service/src/main/java/service/task/manager/service/impl/EpicServiceImpl.java b/service/src/main/java/service/task/manager/service/impl/EpicServiceImpl.java new file mode 100644 index 0000000..9968109 --- /dev/null +++ b/service/src/main/java/service/task/manager/service/impl/EpicServiceImpl.java @@ -0,0 +1,184 @@ +package service.task.manager.service.impl; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import service.task.manager.dto.epic.EpicRequestCreatedDto; +import service.task.manager.dto.epic.EpicRequestUpdatedDto; +import service.task.manager.dto.epic.EpicResponseDto; +import service.task.manager.exception.ConflictException; +import service.task.manager.exception.NotFoundException; +import service.task.manager.mapper.EpicMapper; +import service.task.manager.model.Epic; +import service.task.manager.model.enums.Status; +import service.task.manager.repository.EpicRepository; +import service.task.manager.service.EpicService; +import service.task.manager.service.HistoryService; + +import java.util.Comparator; +import java.util.List; + +/** + * Service implementation for managing epics. + * Provides methods to create, update, retrieve, and delete epics. + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class EpicServiceImpl implements EpicService { + private final EpicRepository repository; + private final EpicMapper mapper; + private final HistoryService history; + + /** + * Creates a new epic based on the provided DTO. + *

+ * This method checks if an epic with the same name already exists in the database. + * If it does, a {@link ConflictException} is thrown. Otherwise, the epic is saved + * with a status of {@link Status#NEW} and the end time calculated based on its start time + * and duration. + *

+ * @param dto The DTO containing epic creation data (must include name, start time, and duration). + * @throws ConflictException If an epic with the same name already exists. + */ + @Transactional + @Override + public void create(EpicRequestCreatedDto dto) { + log.info("Attempting to create epic with name: {}", dto.name()); + if (repository.existsByName(dto.name())) { + log.warn("Epic creation failed: Epic with name {} already exists", dto.name()); + throw new ConflictException("Epic with name " + dto.name() + " already exists"); + } + + Epic epic = addEndTimeEpicAndStatus(mapper.toEntity(dto)); + repository.save(epic); + log.info("Epic created successfully with name: {}", dto.name()); + } + + /** + * Updates an existing epic with the provided data. + *

+ * This method retrieves the epic by its ID, updates its fields using the provided DTO, + * and saves the changes to the database. + *

+ * @param dto The DTO containing updated epic data (must include epic ID). + * @return The updated epic as a DTO. + * @throws NotFoundException If the epic with the specified ID does not exist. + */ + @Transactional + @Override + public EpicResponseDto update(EpicRequestUpdatedDto dto) { + log.info("Attempting to update epic with ID: {}", dto.id()); + Epic existingEpic = mapper.toEntity(findById(dto.id())); + mapper.updateTaskFromDto(dto, existingEpic); + Epic updatedEpic = repository.save(existingEpic); + log.info("Epic with ID {} updated successfully", updatedEpic.getId()); + return mapper.toResponseDto(updatedEpic); + } + + /** + * Retrieves an epic by its ID. + * @param id The ID of the epic to retrieve. + * @return The epic as a DTO. + * @throws NotFoundException If the epic with the specified ID does not exist. + */ + @Transactional(readOnly = true) + @Override + public EpicResponseDto findById(Long id) { + log.info("Fetching epic with ID: {}", id); + EpicResponseDto epic = repository.findById(id) + .stream() + .map(mapper::toResponseDto) + .findFirst() + .orElseThrow(() -> { + log.warn("Epic with ID {} not found", id); + return new NotFoundException("Epic with ID " + id + " not found"); + }); + history.addToHistory(epic.type(), epic.id()); + log.info("Epic with ID {} retrieved successfully", id); + return epic; + } + + /** + * Retrieves all epics. + * @return A list of all epics as DTOs. + */ + @Transactional(readOnly = true) + @Override + public List findAll() { + log.info("Fetching all epics"); + List epics = repository.findAll() + .stream() + .map(mapper::toResponseDto) + .toList(); + log.info("Retrieved {} epics", epics.size()); + return epics; + } + + /** + * Deletes an epic by its ID. + *

+ * This method deletes the epic directly from the database. If the epic does not exist, + * no exception is thrown as per the current implementation. + *

+ * @param id The ID of the epic to delete. + */ + @Transactional + @Override + public void delete(Long id) { + log.info("Attempting to delete epic with ID: {}", id); + repository.deleteById(id); + log.info("Epic with ID {} deleted successfully", id); + } + + /** + * Retrieves all epics sorted by priority and end time. + *

+ * Epics are sorted first by status in the order: IN_PROGRESS, NEW, DONE. + * Within each status group, epics are sorted by end time (earliest first). + *

+ * + * @return A list of all epics as DTOs, sorted by priority and end time. + */ + @Override + public List prioritized() { + log.info("Fetching all epics sorted by priority and end time"); + List epics = repository.findAll() + .stream() + .map(mapper::toResponseDto) + .sorted(Comparator + .comparing(this::getStatusPriority) + .thenComparing(EpicResponseDto::endTime)) + .toList(); + log.info("Retrieved {} prioritized epics", epics.size()); + return epics; + } + + /** + * Helper method to assign priority based on status. + * + * @param dto The epic DTO. + * @return The priority value (lower means higher priority). + */ + private int getStatusPriority(EpicResponseDto dto) { + return switch (dto.status()) { + case IN_PROGRESS -> 1; + case NEW -> 2; + case DONE -> 3; + }; + } + + /** + * Sets the status to NEW and calculates the end time for the given epic. + * @param epic The epic to modify. + * @return The modified epic with updated status and end time. + */ + private Epic addEndTimeEpicAndStatus(Epic epic) { + log.debug("Setting status and calculating end time for epic"); + epic.calculateEndTime(); + epic.setStatus(Status.NEW); + log.debug("Epic status set to NEW and end time calculated"); + return epic; + } +} \ No newline at end of file diff --git a/service/src/main/java/service/task/manager/service/impl/HistoryServiceImpl.java b/service/src/main/java/service/task/manager/service/impl/HistoryServiceImpl.java new file mode 100644 index 0000000..22146dc --- /dev/null +++ b/service/src/main/java/service/task/manager/service/impl/HistoryServiceImpl.java @@ -0,0 +1,83 @@ +package service.task.manager.service.impl; + +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.ListOperations; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; +import service.task.manager.model.HistoryEntry; +import service.task.manager.model.enums.TaskType; +import service.task.manager.service.HistoryService; + +import java.util.ArrayList; +import java.util.List; + +/** + * Implementation of the service for managing the history of accesses to tasks (Task, Epic, Subtask) using Redis. + * The history stores the last 10 records of calls to the findBy(long id) method for tasks, epics, and subtasks. + * If the number of records exceeds the limit, the oldest record is removed. + */ +@Slf4j +@Service +public class HistoryServiceImpl implements HistoryService { + + private static final String HISTORY_KEY = "history"; + private static final int HISTORY_SIZE = 10; + + private final RedisTemplate redisTemplate; + private final ListOperations listOps; + + @Autowired + public HistoryServiceImpl(RedisTemplate redisTemplate) { + this.redisTemplate = redisTemplate; + this.listOps = redisTemplate.opsForList(); + } + + /** + * Adds a record to the history of findBy(long id) method calls. + * If the history size exceeds the limit (10 records), the oldest record is removed. + * + * @param type the type of the task (TASK, EPIC, SUBTASK) + * @param id the identifier of the task + */ + @Override + public void addToHistory(TaskType type, Long id) { + try { + HistoryEntry entry = new HistoryEntry(type, id); + log.info("Adding entry to history: type={}, id={}", type, id); + listOps.rightPush(HISTORY_KEY, entry); + Long size = listOps.size(HISTORY_KEY); + if (size != null && size > HISTORY_SIZE) { + log.debug("History size exceeded limit ({}), removing oldest entry", HISTORY_SIZE); + listOps.leftPop(HISTORY_KEY); + } + } catch (Exception e) { + log.error("Failed to add entry to history: type={}, id={}, exception={}", type, id, e.getMessage(), e); + } + } + + /** + * Retrieves the list of entries from the history of method calls. + * If the history is empty or an exception occurs while retrieving data, an empty list is returned. + * + * @return the list of history entries + */ + @Override + public List getHistory() { + try { + log.info("Retrieving call history"); + List history = listOps.range(HISTORY_KEY, 0, -1); + if (history == null) { + log.warn("History is empty or failed to retrieve data from Redis"); + return new ArrayList<>(); + } + log.debug("Successfully retrieved {} entries from history", history.size()); + return history; + } catch (Exception e) { + log.error("Failed to retrieve history: {}", e.getMessage(), e); + return new ArrayList<>(); + } + } +} \ No newline at end of file diff --git a/service/src/main/java/service/task/manager/service/impl/SubtaskServiceImpl.java b/service/src/main/java/service/task/manager/service/impl/SubtaskServiceImpl.java new file mode 100644 index 0000000..ad4df3c --- /dev/null +++ b/service/src/main/java/service/task/manager/service/impl/SubtaskServiceImpl.java @@ -0,0 +1,192 @@ +package service.task.manager.service.impl; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import service.task.manager.dto.epic.EpicResponseDto; +import service.task.manager.dto.subtask.SubtaskRequestCreatedDto; +import service.task.manager.dto.subtask.SubtaskRequestUpdatedDto; +import service.task.manager.dto.subtask.SubtaskResponseDto; +import service.task.manager.exception.ConflictException; +import service.task.manager.exception.NotFoundException; +import service.task.manager.mapper.EpicMapper; +import service.task.manager.mapper.SubtaskMapper; +import service.task.manager.model.Epic; +import service.task.manager.model.Subtask; +import service.task.manager.model.enums.Status; +import service.task.manager.repository.SubtaskRepository; +import service.task.manager.service.EpicService; +import service.task.manager.service.HistoryService; +import service.task.manager.service.SubtaskService; + +import java.util.Comparator; +import java.util.List; + +/** + * Service implementation for managing subtasks. + * Provides methods to create, update, retrieve, and delete subtasks associated with epics. + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class SubtaskServiceImpl implements SubtaskService { + private final SubtaskRepository repository; + private final EpicService epicService; + private final SubtaskMapper mapper; + private final EpicMapper epicMapper; + private final HistoryService history; + + /** + * Creates a new subtask based on the provided DTO. + *

+ * This method verifies the existence of the associated epic and checks for duplicate subtask names. + * If the epic does not exist, a {@link NotFoundException} will be thrown by the EpicService. + * If a subtask with the same name already exists, a {@link ConflictException} is thrown. + *

+ * @param dto The DTO containing subtask creation data (must include name and epic ID). + * @throws ConflictException If a subtask with the same name already exists. + * @throws NotFoundException If the associated epic does not exist. + */ + @Transactional + @Override + public void create(SubtaskRequestCreatedDto dto) { + log.info("Attempting to create subtask with name: {}", dto.name()); + + EpicResponseDto epicDto = epicService.findById(dto.epicId()); + Epic epic = epicMapper.toEntity(epicDto); + + if (repository.existsByName(dto.name())) { + log.warn("Subtask creation failed: Subtask with name {} already exists", dto.name()); + throw new ConflictException("Subtask with name " + dto.name() + " already exists"); + } + + Subtask subtask = addEndTimeSubtaskAndStatus(mapper.toEntity(dto)); + subtask.setEpic(epic); + repository.save(subtask); + log.info("Subtask created successfully with name: {}", dto.name()); + } + + /** + * Updates an existing subtask with the provided data. + *

+ * This method retrieves the subtask by its ID, updates its fields using the provided DTO, + * and saves the changes to the database. + *

+ * @param dto The DTO containing updated subtask data (must include subtask ID). + * @return The updated subtask as a DTO. + * @throws NotFoundException If the subtask with the specified ID does not exist. + */ + @Transactional + @Override + public SubtaskResponseDto update(SubtaskRequestUpdatedDto dto) { + log.info("Attempting to update subtask with ID: {}", dto.id()); + + Subtask existingSubtask = mapper.toEntity(findById(dto.id())); + mapper.updateSubtaskFromDto(dto, existingSubtask); + Subtask updatedSubtask = repository.save(existingSubtask); + log.info("Subtask with ID {} updated successfully", updatedSubtask.getId()); + return mapper.toResponseDto(updatedSubtask); + } + + /** + * Retrieves a subtask by its ID. + * @param id The ID of the subtask to retrieve. + * @return The subtask as a DTO. + * @throws NotFoundException If the subtask with the specified ID does not exist. + */ + @Transactional(readOnly = true) + @Override + public SubtaskResponseDto findById(Long id) { + log.info("Fetching subtask with ID: {}", id); + SubtaskResponseDto subtask = repository.findById(id).stream() + .map(mapper::toResponseDto) + .findFirst() + .orElseThrow(() -> { + log.warn("Subtask with ID {} not found", id); + return new NotFoundException("Subtask with ID " + id + " not found"); + }); + history.addToHistory(subtask.type(),subtask.id()); + log.info("Subtask with ID {} retrieved successfully", id); + return subtask; + } + + /** + * Retrieves all subtasks. + * @return A list of all subtasks as DTOs. + */ + @Transactional(readOnly = true) + @Override + public List findAll() { + log.info("Fetching all subtasks"); + List subtasks = repository.findAll().stream() + .map(mapper::toResponseDto) + .toList(); + log.info("Retrieved {} subtasks", subtasks.size()); + return subtasks; + } + + /** + * Deletes a subtask by its ID. + * @param id The ID of the subtask to delete. + * @throws NotFoundException If the subtask with the specified ID does not exist. + */ + @Transactional + @Override + public void delete(Long id) { + log.info("Attempting to delete subtask with ID: {}", id); + findById(id); // Проверяет существование + repository.deleteById(id); + log.info("Subtask with ID {} deleted successfully", id); + } + + /** + * Retrieves all subtasks sorted by priority and end time. + *

+ * Epics are sorted first by status in the order: IN_PROGRESS, NEW, DONE. + * Within each status group, subtasks are sorted by end time (earliest first). + *

+ * + * @return A list of all subtasks as DTOs, sorted by priority and end time. + */ + @Override + public List prioritized() { + log.info("Fetching all subtasks sorted by priority and end time"); + List subtask = repository.findAll() + .stream() + .map(mapper::toResponseDto) + .sorted(Comparator + .comparing(this::getStatusPriority) + .thenComparing(SubtaskResponseDto::endTime)) + .toList(); + log.info("Retrieved {} prioritized subtasks", subtask.size()); + return subtask; + } + + /** + * Helper method to assign priority based on status. + * + * @param dto The subtask DTO. + * @return The priority value (lower means higher priority). + */ + private int getStatusPriority(SubtaskResponseDto dto) { + return switch (dto.status()) { + case IN_PROGRESS -> 1; + case NEW -> 2; + case DONE -> 3; + }; + } + + /** + * Sets the status to NEW and calculates the end time for the given subtask. + * @param subtask The subtask to modify. + * @return The modified subtask with updated status and end time. + */ + private Subtask addEndTimeSubtaskAndStatus(Subtask subtask) { + log.debug("Setting status and calculating end time for subtask"); + subtask.calculateEndTime(); + subtask.setStatus(Status.NEW); + log.debug("Subtask status set to NEW and end time calculated"); + return subtask; + } +} \ No newline at end of file diff --git a/service/src/main/java/service/task/manager/service/impl/TaskServiceImpl.java b/service/src/main/java/service/task/manager/service/impl/TaskServiceImpl.java new file mode 100644 index 0000000..3556d49 --- /dev/null +++ b/service/src/main/java/service/task/manager/service/impl/TaskServiceImpl.java @@ -0,0 +1,170 @@ +package service.task.manager.service.impl; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import service.task.manager.dto.task.TaskRequestCreatedDto; +import service.task.manager.dto.task.TaskRequestUpdatedDto; +import service.task.manager.dto.task.TaskResponseDto; +import service.task.manager.exception.ConflictException; +import service.task.manager.exception.NotFoundException; +import service.task.manager.mapper.TaskMapper; +import service.task.manager.model.Task; +import service.task.manager.model.enums.Status; +import service.task.manager.repository.TaskRepository; +import service.task.manager.service.HistoryService; +import service.task.manager.service.TaskService; + +import java.util.Comparator; +import java.util.List; + +/** + * Service implementation for managing tasks. + * Provides methods to create, update, retrieve, and delete tasks. + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class TaskServiceImpl implements TaskService { + private final TaskRepository repository; + private final TaskMapper mapper; + private final HistoryService history; + + /** + * Creates a new task based on the provided DTO. + * @param dto The DTO containing task creation data. + * @throws ConflictException If a task with the same name already exists. + */ + @Transactional + @Override + public void create(TaskRequestCreatedDto dto) { + log.info("Attempting to create task with name: {}", dto.name()); + if (repository.existsByName(dto.name())) { + log.warn("Task creation failed: Task with name {} already exists", dto.name()); + throw new ConflictException("Task with name " + dto.name() + " already exists"); + } + + Task task = addEndTimeTaskAndStatus(mapper.toEntity(dto)); + repository.save(task); + log.info("Task created successfully with name: {}", dto.name()); + } + + /** + * Updates an existing task with the provided data. + * @param dto The DTO containing updated task data. + * @return The updated task as a DTO. + * @throws NotFoundException If the task with the specified ID does not exist. + */ + @Transactional + @Override + public TaskResponseDto update(TaskRequestUpdatedDto dto) { + log.info("Attempting to update task with ID: {}", dto.id()); + Task existingTask = mapper.toEntity(findById(dto.id())); + mapper.updateTaskFromDto(dto, existingTask); + Task updatedTask = repository.save(existingTask); + log.info("Task with ID {} updated successfully", updatedTask.getId()); + return mapper.toResponseDto(updatedTask); + } + + /** + * Retrieves a task by its ID. + * @param id The ID of the task to retrieve. + * @return The task as a DTO. + * @throws NotFoundException If the task with the specified ID does not exist. + */ + @Transactional(readOnly = true) + @Override + public TaskResponseDto findById(Long id) { + log.info("Fetching task with ID: {}", id); + TaskResponseDto task = repository.findById(id).stream() + .map(mapper::toResponseDto) + .findFirst() + .orElseThrow(() -> { + log.warn("Task with ID {} not found", id); + return new NotFoundException("Task with ID " + id + " not found"); + }); + history.addToHistory(task.type(),task.id()); + log.info("Task with ID {} retrieved successfully", id); + return task; + } + + /** + * Retrieves all tasks. + * @return A list of all tasks as DTOs. + */ + @Transactional(readOnly = true) + @Override + public List findAll() { + log.info("Fetching all tasks"); + List tasks = repository.findAll().stream() + .map(mapper::toResponseDto) + .toList(); + log.info("Retrieved {} tasks", tasks.size()); + return tasks; + } + + /** + * Deletes a task by its ID. + * @param id The ID of the task to delete. + * @throws NotFoundException If the task with the specified ID does not exist. + */ + @Transactional + @Override + public void delete(Long id) { + log.info("Attempting to delete task with ID: {}", id); + findById(id); // Проверяет существование + repository.deleteById(id); + log.info("Task with ID {} deleted successfully", id); + } + + /** + * Retrieves all tasks sorted by priority and end time. + *

+ * Epics are sorted first by status in the order: IN_PROGRESS, NEW, DONE. + * Within each status group, tasks are sorted by end time (earliest first). + *

+ * + * @return A list of all tasks as DTOs, sorted by priority and end time. + */ + @Override + public List prioritized() { + log.info("Fetching all subtasks sorted by priority and end time"); + List tasks = repository.findAll() + .stream() + .map(mapper::toResponseDto) + .sorted(Comparator + .comparing(this::getStatusPriority) + .thenComparing(TaskResponseDto::endTime)) + .toList(); + log.info("Retrieved {} prioritized subtasks", tasks.size()); + return tasks; + } + + /** + * Helper method to assign priority based on status. + * + * @param dto The task DTO. + * @return The priority value (lower means higher priority). + */ + private int getStatusPriority(TaskResponseDto dto) { + return switch (dto.status()) { + case IN_PROGRESS -> 1; + case NEW -> 2; + case DONE -> 3; + }; + } + + /** + * Sets the status to NEW and calculates the end time for the given task. + * @param task The task to modify. + * @return The modified task with updated status and end time. + */ + private Task addEndTimeTaskAndStatus(Task task) { + log.debug("Setting status and calculating end time for task"); + task.calculateEndTime(); + task.setStatus(Status.NEW); + log.debug("Task status set to NEW and end time calculated"); + return task; + } +} \ No newline at end of file diff --git a/service/src/main/resources/application.yml b/service/src/main/resources/application.yml new file mode 100644 index 0000000..cd1718f --- /dev/null +++ b/service/src/main/resources/application.yml @@ -0,0 +1,30 @@ +spring: + datasource: + url: jdbc:h2:mem:task-manager + username: sa + password: + driver-class-name: org.h2.Driver + sql: + init: + mode: always + schema-locations: classpath:schema.sql + jpa: + properties: + hibernate.dialect: org.hibernate.dialect.H2Dialect + data: + redis: + host: localhost + port: 6379 + +# Путь к Swagger UI +springdoc: + swagger-ui: + path: /swagger-ui.html + tags-sorter: alpha + operations-sorter: alpha + info: + title: Task Manager API + version: 1.0.0 + api-docs: + enabled: true + path: /v3/api-docs \ No newline at end of file diff --git a/service/src/main/resources/schema.sql b/service/src/main/resources/schema.sql new file mode 100644 index 0000000..4135a0a --- /dev/null +++ b/service/src/main/resources/schema.sql @@ -0,0 +1,45 @@ +-- Таблица для Epic +CREATE TABLE epic +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + description TEXT, + status VARCHAR(50), + start_time TIMESTAMP, + duration BIGINT, -- Хранится в секундах + end_time TIMESTAMP, + type VARCHAR(50) NOT NULL +); + +-- Таблица для Subtask +CREATE TABLE subtask +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + epic_id BIGINT, + name VARCHAR(255) NOT NULL, + description TEXT, + status VARCHAR(50), + start_time TIMESTAMP, + duration BIGINT, -- Хранится в секундах + end_time TIMESTAMP, + type VARCHAR(50) NOT NULL, + FOREIGN KEY (epic_id) REFERENCES epic (id) ON DELETE CASCADE +); + +-- Таблица для Task +CREATE TABLE task +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + description TEXT, + status VARCHAR(50), + start_time TIMESTAMP, + duration BIGINT, -- Хранится в секундах + end_time TIMESTAMP, + type VARCHAR(50) NOT NULL +); + +-- Опционально: Добавление индексов для оптимизации +CREATE INDEX idx_epic_status ON epic (status); +CREATE INDEX idx_subtask_epic_id ON subtask (epic_id); +CREATE INDEX idx_task_status ON task (status); \ No newline at end of file diff --git a/service/src/test/java/service/task/manager/controller/EpicControllerTest.java b/service/src/test/java/service/task/manager/controller/EpicControllerTest.java new file mode 100644 index 0000000..4fde163 --- /dev/null +++ b/service/src/test/java/service/task/manager/controller/EpicControllerTest.java @@ -0,0 +1,253 @@ +package service.task.manager.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import service.task.manager.dto.epic.EpicRequestCreatedDto; +import service.task.manager.dto.epic.EpicRequestUpdatedDto; +import service.task.manager.dto.epic.EpicResponseDto; +import service.task.manager.exception.ConflictException; +import service.task.manager.exception.NotFoundException; +import service.task.manager.model.enums.Status; +import service.task.manager.model.enums.TaskType; +import service.task.manager.service.EpicService; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.List; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@WebMvcTest(EpicController.class) +class EpicControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private EpicService epicService; + + @Autowired + private ObjectMapper objectMapper; + + private EpicResponseDto epicResponseDto; + + @BeforeEach + void setUp() { + epicResponseDto = new EpicResponseDto( + 1L, + Collections.emptyList(), // subtasks + "Test Epic", + "Test Description", + Status.NEW, + LocalDateTime.of(2025, 4, 27, 10, 0), + Duration.ofHours(24), + LocalDateTime.of(2025, 4, 28, 10, 0), + TaskType.EPIC + ); + } + + @Test + void create_ValidEpic_ReturnsCreated() throws Exception { + EpicRequestCreatedDto requestDto = new EpicRequestCreatedDto( + "Test Epic", + "Test Description", + LocalDateTime.of(2025, 4, 27, 10, 0), + Duration.ofHours(24) + ); + + doNothing().when(epicService).create(any(EpicRequestCreatedDto.class)); + + mockMvc.perform(post("/epic") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestDto))) + .andExpect(status().isCreated()) + .andExpect(content().string("")); + + verify(epicService, times(1)).create(any(EpicRequestCreatedDto.class)); + } + + @Test + void create_DuplicateEpic_ReturnsConflict() throws Exception { + EpicRequestCreatedDto requestDto = new EpicRequestCreatedDto( + "Duplicate Epic", + "Test Description", + LocalDateTime.of(2025, 4, 27, 10, 0), + Duration.ofHours(24) + ); + + doThrow(new ConflictException("Epic with the same name already exists")) + .when(epicService).create(any(EpicRequestCreatedDto.class)); + + mockMvc.perform(post("/epic") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestDto))) + .andExpect(status().isConflict()) + .andExpect(jsonPath("$.message").value("Epic with the same name already exists")); + + verify(epicService, times(1)).create(any(EpicRequestCreatedDto.class)); + } + + @Test + void create_InvalidEpic_ReturnsBadRequest() throws Exception { + EpicRequestCreatedDto requestDto = new EpicRequestCreatedDto( + "", // Пустое имя, что нарушает валидацию + "Test Description", + LocalDateTime.of(2025, 4, 27, 10, 0), + Duration.ofHours(24) + ); + + mockMvc.perform(post("/epic") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestDto))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.name").value("blank name")); + + verify(epicService, never()).create(any(EpicRequestCreatedDto.class)); + } + + @Test + void update_ValidEpic_ReturnsOk() throws Exception { + EpicRequestUpdatedDto requestDto = new EpicRequestUpdatedDto( + 1L, + "Updated Epic", + "Updated Description", + Status.IN_PROGRESS, + Duration.ofHours(48) + ); + + when(epicService.update(any(EpicRequestUpdatedDto.class))).thenReturn(epicResponseDto); + + mockMvc.perform(put("/epic") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestDto))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(1)) + .andExpect(jsonPath("$.name").value("Test Epic")) + .andExpect(jsonPath("$.status").value("NEW")); + + verify(epicService, times(1)).update(any(EpicRequestUpdatedDto.class)); + } + + @Test + void update_EpicNotFound_ReturnsNotFound() throws Exception { + EpicRequestUpdatedDto requestDto = new EpicRequestUpdatedDto( + 999L, + "Updated Epic", + "Updated Description", + Status.IN_PROGRESS, + Duration.ofHours(48) + ); + + when(epicService.update(any(EpicRequestUpdatedDto.class))) + .thenThrow(new NotFoundException("Epic not found")); + + mockMvc.perform(put("/epic") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestDto))) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.message").value("Epic not found")); + + verify(epicService, times(1)).update(any(EpicRequestUpdatedDto.class)); + } + + @Test + void findById_ValidId_ReturnsOk() throws Exception { + when(epicService.findById(1L)).thenReturn(epicResponseDto); + + mockMvc.perform(get("/epic/1")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(1)) + .andExpect(jsonPath("$.name").value("Test Epic")) + .andExpect(jsonPath("$.status").value("NEW")) + .andExpect(jsonPath("$.type").value("EPIC")); + + verify(epicService, times(1)).findById(1L); + } + + @Test + void findById_EpicNotFound_ReturnsNotFound() throws Exception { + when(epicService.findById(999L)).thenThrow(new NotFoundException("Epic not found")); + + mockMvc.perform(get("/epic/999")) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.message").value("Epic not found")); + + verify(epicService, times(1)).findById(999L); + } + + @Test + void findById_InvalidId_ReturnsBadRequest() throws Exception { + mockMvc.perform(get("/epic/-1")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.error").value("id must be positive")); + + verify(epicService, never()).findById(anyLong()); + } + + @Test + void findAll_ReturnsOk() throws Exception { + List epics = List.of(epicResponseDto); + when(epicService.findAll()).thenReturn(epics); + + mockMvc.perform(get("/epic")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id").value(1)) + .andExpect(jsonPath("$[0].name").value("Test Epic")) + .andExpect(jsonPath("$[0].status").value("NEW")) + .andExpect(jsonPath("$[0].type").value("EPIC")); + + verify(epicService, times(1)).findAll(); + } + + @Test + void findAll_EmptyList_ReturnsOk() throws Exception { + when(epicService.findAll()).thenReturn(Collections.emptyList()); + + mockMvc.perform(get("/epic")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$").isArray()) + .andExpect(jsonPath("$").isEmpty()); + + verify(epicService, times(1)).findAll(); + } + + @Test + void delete_ValidId_ReturnsNoContent() throws Exception { + doNothing().when(epicService).delete(1L); + + mockMvc.perform(delete("/epic/1")) + .andExpect(status().isNoContent()); + + verify(epicService, times(1)).delete(1L); + } + + @Test + void delete_EpicNotFound_ReturnsNotFound() throws Exception { + doThrow(new NotFoundException("Epic not found")).when(epicService).delete(999L); + + mockMvc.perform(delete("/epic/999")) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.message").value("Epic not found")); + + verify(epicService, times(1)).delete(999L); + } + + @Test + void delete_InvalidId_ReturnsBadRequest() throws Exception { + mockMvc.perform(delete("/epic/-1")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.error").value("id must be positive")); + + verify(epicService, never()).delete(anyLong()); + } +} \ No newline at end of file diff --git a/service/src/test/java/service/task/manager/controller/SubtaskControllerTest.java b/service/src/test/java/service/task/manager/controller/SubtaskControllerTest.java new file mode 100644 index 0000000..40d03a4 --- /dev/null +++ b/service/src/test/java/service/task/manager/controller/SubtaskControllerTest.java @@ -0,0 +1,256 @@ +package service.task.manager.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import service.task.manager.dto.subtask.SubtaskRequestCreatedDto; +import service.task.manager.dto.subtask.SubtaskRequestUpdatedDto; +import service.task.manager.dto.subtask.SubtaskResponseDto; +import service.task.manager.exception.ConflictException; +import service.task.manager.exception.ErrorHandler; +import service.task.manager.exception.NotFoundException; +import service.task.manager.model.enums.Status; +import service.task.manager.model.enums.TaskType; +import service.task.manager.service.SubtaskService; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.List; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@WebMvcTest(controllers = {SubtaskController.class, ErrorHandler.class}) +class SubtaskControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private SubtaskService subtaskService; + + private final LocalDateTime testTime = LocalDateTime.of(2025, 4, 27, 10, 0); + private final Duration testDuration = Duration.ofHours(24); + + // CREATE tests + @Test + void createSubtask_ValidRequest_ReturnsCreated() throws Exception { + SubtaskRequestCreatedDto request = new SubtaskRequestCreatedDto( + 1L, + "Valid Subtask", + "Valid Description", + testTime, + testDuration + ); + + mockMvc.perform(post("/subtask") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isCreated()) + .andExpect(content().string("")); + + verify(subtaskService).create(request); + } + + @Test + void createSubtask_ConflictName_ReturnsConflict() throws Exception { + SubtaskRequestCreatedDto request = new SubtaskRequestCreatedDto( + 1L, + "Duplicate Subtask", + "Description", + testTime, + testDuration + ); + + doThrow(new ConflictException("Subtask with name Duplicate Subtask already exists")) + .when(subtaskService).create(request); // Используем конкретный request + + mockMvc.perform(post("/subtask") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isConflict()) + .andExpect(jsonPath("$.message").value("Subtask with name Duplicate Subtask already exists")); + } + + // UPDATE tests + @Test + void updateSubtask_ValidRequest_ReturnsOk() throws Exception { + SubtaskRequestUpdatedDto request = new SubtaskRequestUpdatedDto( + 1L, + "Updated Subtask", + "Updated Description", + Status.IN_PROGRESS, + Duration.ofHours(48) + ); + + SubtaskResponseDto responseDto = new SubtaskResponseDto( + 1L, + 1L, + "Updated Subtask", + "Updated Description", + Status.IN_PROGRESS, + testTime, + testTime.plus(Duration.ofHours(48)), + Duration.ofHours(48), + TaskType.SUBTASK + ); + + when(subtaskService.update(any())).thenReturn(responseDto); + + mockMvc.perform(put("/subtask") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(1L)) + .andExpect(jsonPath("$.name").value("Updated Subtask")) + .andDo(result -> System.out.println("Response: " + result.getResponse().getContentAsString())); + + verify(subtaskService).update(request); // Проверяем вызов сервиса + } + + @Test + void updateSubtask_NotFound_ReturnsNotFound() throws Exception { + SubtaskRequestUpdatedDto request = new SubtaskRequestUpdatedDto( + 999L, + "Non-existent Subtask", + "Description", + Status.NEW, + testDuration + ); + + when(subtaskService.update(any())) + .thenThrow(new NotFoundException("Subtask with ID 999 not found")); + + mockMvc.perform(put("/subtask") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.message").value("Subtask with ID 999 not found")); + } + + // GET tests + @Test + void getSubtask_ValidId_ReturnsOk() throws Exception { + SubtaskResponseDto responseDto = new SubtaskResponseDto( + 1L, + 1L, + "Test Subtask", + "Test Description", + Status.NEW, + testTime, + testTime.plus(testDuration), + testDuration, + TaskType.SUBTASK + ); + + when(subtaskService.findById(1L)).thenReturn(responseDto); + + mockMvc.perform(get("/subtask/1")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(1L)) + .andExpect(jsonPath("$.epicId").value(1L)) + .andExpect(jsonPath("$.name").value("Test Subtask")) + .andExpect(jsonPath("$.description").value("Test Description")) + .andExpect(jsonPath("$.status").value("NEW")) + .andExpect(jsonPath("$.startTime").value("2025-04-27T10:00:00")) + .andExpect(jsonPath("$.endTime").value("2025-04-28T10:00:00")) + .andExpect(jsonPath("$.duration").value("PT24H")) + .andExpect(jsonPath("$.type").value("SUBTASK")) + .andDo(result -> System.out.println("Response: " + result.getResponse().getContentAsString())); + } + + @Test + void getSubtask_InvalidId_ReturnsBadRequest() throws Exception { + mockMvc.perform(get("/subtask/-1")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.error").value("id must be positive")) + .andDo(result -> System.out.println("Response: " + result.getResponse().getContentAsString())); + } + + @Test + void getSubtask_NotFound_ReturnsNotFound() throws Exception { + when(subtaskService.findById(999L)) + .thenThrow(new NotFoundException("Subtask with ID 999 not found")); + + mockMvc.perform(get("/subtask/999")) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.message").value("Subtask with ID 999 not found")); + } + + // GET ALL tests + @Test + void getAllSubtasks_ReturnsOk() throws Exception { + List subtasks = Collections.singletonList( + new SubtaskResponseDto( + 1L, + 1L, + "Subtask", + "Description", + Status.NEW, + testTime, + testTime.plus(testDuration), + testDuration, + TaskType.SUBTASK + ) + ); + + when(subtaskService.findAll()).thenReturn(subtasks); + + mockMvc.perform(get("/subtask")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id").value(1L)) + .andDo(result -> System.out.println("Response: " + result.getResponse().getContentAsString())); + } + + // DELETE tests + @Test + void deleteSubtask_ValidId_ReturnsNoContent() throws Exception { + mockMvc.perform(delete("/subtask/1")) + .andExpect(status().isNoContent()) + .andExpect(content().string("")); // Проверяем, что тело ответа пустое + + verify(subtaskService).delete(1L); + } + + @Test + void deleteSubtask_NotFound_ReturnsNotFound() throws Exception { + doThrow(new NotFoundException("Subtask with ID 999 not found")) + .when(subtaskService).delete(999L); + + mockMvc.perform(delete("/subtask/999")) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.message").value("Subtask with ID 999 not found")); + } + + // Validation tests + @Test + void createSubtask_InvalidRequest_ReturnsBadRequest() throws Exception { + SubtaskRequestCreatedDto invalidRequest = new SubtaskRequestCreatedDto( + null, // Missing epicId + "", // Blank name + "", // Blank description + null, // Missing startTime + null // Missing duration + ); + + mockMvc.perform(post("/subtask") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(invalidRequest))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.epicId").value("null epic ID")) + .andExpect(jsonPath("$.name").value("blank name")) + .andExpect(jsonPath("$.description").value("blank description")) + .andExpect(jsonPath("$.startTime").value("null start time")) + .andExpect(jsonPath("$.duration").value("null duration")); + } +} \ No newline at end of file diff --git a/service/src/test/java/service/task/manager/controller/TaskControllerTest.java b/service/src/test/java/service/task/manager/controller/TaskControllerTest.java new file mode 100644 index 0000000..bfff349 --- /dev/null +++ b/service/src/test/java/service/task/manager/controller/TaskControllerTest.java @@ -0,0 +1,223 @@ +package service.task.manager.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import service.task.manager.dto.task.TaskRequestCreatedDto; +import service.task.manager.dto.task.TaskRequestUpdatedDto; +import service.task.manager.dto.task.TaskResponseDto; +import service.task.manager.exception.ErrorHandler; +import service.task.manager.exception.NotFoundException; +import service.task.manager.model.enums.Status; +import service.task.manager.model.enums.TaskType; +import service.task.manager.service.TaskService; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.List; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(controllers = {TaskController.class, ErrorHandler.class}) +class TaskControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private TaskService taskService; + + private final LocalDateTime testTime = LocalDateTime.of(2025, 4, 27, 10, 0); + private final Duration testDuration = Duration.ofHours(24); + + // CREATE tests + @Test + void createTask_ValidRequest_ReturnsCreated() throws Exception { + TaskRequestCreatedDto request = new TaskRequestCreatedDto( + "Valid Task", + "Valid Description", + testTime, + testDuration + ); + + mockMvc.perform(post("/task") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isCreated()); + + verify(taskService).create(request); + } + + @Test + void createTask_InvalidRequest_ReturnsBadRequest() throws Exception { + TaskRequestCreatedDto invalidRequest = new TaskRequestCreatedDto( + "", // Invalid blank name + "", // Invalid blank description + null, // Missing start time + null // Missing duration + ); + + mockMvc.perform(post("/task") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(invalidRequest))) + .andExpect(status().isBadRequest()); + } + @Test + void updateTask_ValidRequest_ReturnsOk() throws Exception { + TaskRequestUpdatedDto request = new TaskRequestUpdatedDto( + 1L, + "Updated Task", + "Updated Description", + Status.IN_PROGRESS, + Duration.ofHours(48) + ); + + TaskResponseDto responseDto = new TaskResponseDto( + 1L, + "Updated Task", + "Updated Description", + Status.IN_PROGRESS, + testTime, + testTime.plus(Duration.ofHours(48)), + Duration.ofHours(48), + TaskType.TASK + ); + + when(taskService.update(any(TaskRequestUpdatedDto.class))).thenReturn(responseDto); + + mockMvc.perform(put("/task") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(1L)) + .andExpect(jsonPath("$.name").value("Updated Task")); + } + + @Test + void updateTask_InvalidId_ReturnsBadRequest() throws Exception { + TaskRequestUpdatedDto invalidRequest = new TaskRequestUpdatedDto( + -1L, // Invalid ID + "Task", + "Description", + Status.NEW, + testDuration + ); + + mockMvc.perform(put("/task") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(invalidRequest))) + .andExpect(status().isBadRequest()); + } + + // GET tests + @Test + void getTask_ValidId_ReturnsOk() throws Exception { + TaskResponseDto responseDto = new TaskResponseDto( + 1L, + "Test Task", + "Test Description", + Status.NEW, + testTime, + testTime.plus(testDuration), + testDuration, + TaskType.TASK + ); + + when(taskService.findById(1L)).thenReturn(responseDto); + + mockMvc.perform(get("/task/1")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(1L)); + } + + @Test + void getTask_InvalidId_ReturnsBadRequest() throws Exception { + mockMvc.perform(get("/task/-1")) + .andExpect(status().isBadRequest()); + } + + // GET ALL tests + @Test + void getAllTasks_ReturnsOk() throws Exception { + List tasks = Collections.singletonList( + new TaskResponseDto( + 1L, + "Task", + "Description", + Status.NEW, + testTime, + testTime.plus(testDuration), + testDuration, + TaskType.TASK + ) + ); + + when(taskService.findAll()).thenReturn(tasks); + + mockMvc.perform(get("/task")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id").value(1L)); + } + + // DELETE tests + @Test + void deleteTask_ValidId_ReturnsNoContent() throws Exception { + mockMvc.perform(delete("/task/1")) + .andExpect(status().isNoContent()); + + verify(taskService).delete(1L); + } + + @Test + void deleteTask_InvalidId_ReturnsBadRequest() throws Exception { + mockMvc.perform(delete("/task/-1")) + .andExpect(status().isBadRequest()); + } + + // Exception handling tests + @Test + void updateTask_NotFound_ReturnsNotFound() throws Exception { + // Arrange + Long nonExistentTaskId = 999L; + TaskRequestUpdatedDto request = new TaskRequestUpdatedDto( + nonExistentTaskId, + "Non-existent Task", + "Description", + Status.NEW, + testDuration + ); + + String expectedErrorMessage = "Task with ID " + nonExistentTaskId + " not found"; + + when(taskService.update(any(TaskRequestUpdatedDto.class))) + .thenThrow(new NotFoundException(expectedErrorMessage)); + mockMvc.perform(put("/task") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.message").value(expectedErrorMessage)); + + verify(taskService).update(any(TaskRequestUpdatedDto.class)); + } + + @Test + void getTask_NotFound_ReturnsNotFound() throws Exception { + when(taskService.findById(999L)).thenThrow(new NotFoundException("Task not found")); + + mockMvc.perform(get("/task/999")) + .andExpect(status().isNotFound()); + } +} \ No newline at end of file diff --git a/service/src/test/java/service/task/manager/service/EpicServiceImplTest.java b/service/src/test/java/service/task/manager/service/EpicServiceImplTest.java new file mode 100644 index 0000000..4be2639 --- /dev/null +++ b/service/src/test/java/service/task/manager/service/EpicServiceImplTest.java @@ -0,0 +1,235 @@ +package service.task.manager.service; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import service.task.manager.dto.epic.EpicRequestCreatedDto; +import service.task.manager.dto.epic.EpicRequestUpdatedDto; +import service.task.manager.dto.epic.EpicResponseDto; +import service.task.manager.exception.ConflictException; +import service.task.manager.exception.NotFoundException; +import service.task.manager.mapper.EpicMapper; +import service.task.manager.model.Epic; +import service.task.manager.model.enums.Status; +import service.task.manager.model.enums.TaskType; +import service.task.manager.repository.EpicRepository; +import service.task.manager.service.impl.EpicServiceImpl; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class EpicServiceImplTest { + + @Mock + private EpicRepository repository; + + @Mock + private EpicMapper mapper; + + @Mock + private HistoryService history; + + @InjectMocks + private EpicServiceImpl service; + + // Sample data for tests + private final LocalDateTime now = LocalDateTime.now(); + private final Duration duration = Duration.ofHours(24); + private final LocalDateTime endTime = now.plus(duration); + + // --- Tests for create method --- + + @Test + void create_ShouldCreateEpic_WhenNameIsUnique() { + // Arrange + EpicRequestCreatedDto dto = new EpicRequestCreatedDto("New Epic", "Test epic created", now, duration); + Epic epic = new Epic(); + epic.setName(dto.name()); + epic.setDescription(dto.description()); + epic.setStartTime(dto.startTime()); + epic.setDuration(dto.duration()); + epic.setStatus(Status.NEW); + epic.setSubtasks(new ArrayList<>()); + + when(repository.existsByName(dto.name())).thenReturn(false); + when(mapper.toEntity(dto)).thenReturn(epic); + when(repository.save(any(Epic.class))).thenReturn(epic); + + // Act + try { + service.create(dto); + } catch (Exception e) { + System.out.println("Exception in create: " + e.getMessage()); + e.printStackTrace(); + throw e; + } + + // Assert + verify(repository, times(1)).existsByName(dto.name()); + verify(mapper, times(1)).toEntity(dto); + verify(repository, times(1)).save(epic); + assertEquals(Status.NEW, epic.getStatus()); + assertEquals(endTime, epic.getEndTime()); + assertEquals(TaskType.EPIC, epic.getType()); + } + + @Test + void create_ShouldThrowConflictException_WhenNameExists() { + // Arrange + EpicRequestCreatedDto dto = new EpicRequestCreatedDto("Existing Epic", "Description", now, duration); + when(repository.existsByName(dto.name())).thenReturn(true); + + // Act & Assert + assertThrows(ConflictException.class, () -> service.create(dto)); + verify(repository, times(1)).existsByName(dto.name()); + verify(mapper, never()).toEntity((EpicRequestCreatedDto) any()); + verify(repository, never()).save(any()); + } + + // --- Tests for update method --- + + @Test + void update_ShouldUpdateEpic_WhenEpicExists() { + // Arrange + Long epicId = 1L; + EpicRequestUpdatedDto dto = new EpicRequestUpdatedDto(epicId, "Updated Epic", "Updated Description", Status.IN_PROGRESS, duration); + Epic existingEpic = new Epic(); + existingEpic.setId(epicId); + existingEpic.setName("Old Epic"); + existingEpic.setDescription("Old Description"); + existingEpic.setStartTime(now.minusDays(1)); + existingEpic.setDuration(Duration.ofHours(12)); + existingEpic.setStatus(Status.NEW); + existingEpic.setSubtasks(new ArrayList<>()); + + EpicResponseDto responseDto = new EpicResponseDto(epicId, new ArrayList<>(), "Updated Epic", "Updated Description", Status.IN_PROGRESS, now.minusDays(1), duration, now.minusDays(1).plus(duration), TaskType.EPIC); + + when(repository.findById(epicId)).thenReturn(Optional.of(existingEpic)); + when(mapper.toResponseDto(existingEpic)).thenReturn(responseDto); + when(mapper.toEntity(responseDto)).thenReturn(existingEpic); + when(repository.save(existingEpic)).thenReturn(existingEpic); + + // Act + EpicResponseDto result = service.update(dto); + + // Assert + verify(repository, times(1)).findById(epicId); + verify(mapper, times(1)).updateTaskFromDto(dto, existingEpic); + verify(repository, times(1)).save(existingEpic); + assertEquals("Updated Epic", result.name()); + assertEquals(now.minusDays(1).plus(duration), result.endTime()); + assertEquals(TaskType.EPIC, result.type()); + } + + @Test + void update_ShouldThrowNotFoundException_WhenEpicDoesNotExist() { + // Arrange + Long epicId = 1L; + EpicRequestUpdatedDto dto = new EpicRequestUpdatedDto(epicId, "Updated Epic", "Updated Description", Status.IN_PROGRESS, duration); + when(repository.findById(epicId)).thenReturn(Optional.empty()); + + // Act & Assert + assertThrows(NotFoundException.class, () -> service.update(dto)); + verify(repository, times(1)).findById(epicId); + verify(mapper, never()).updateTaskFromDto(any(), any()); + verify(repository, never()).save(any()); + } + + // --- Tests for findById method --- + + @Test + void findById_ShouldReturnEpic_WhenEpicExists() { + // Arrange + Long epicId = 1L; + Epic epic = new Epic(); + epic.setId(epicId); + epic.setName("Epic"); + epic.setDescription("Description"); + epic.setStartTime(now); + epic.setDuration(duration); + epic.setEndTime(endTime); + epic.setStatus(Status.NEW); + epic.setSubtasks(new ArrayList<>()); + + EpicResponseDto dto = new EpicResponseDto(epicId, new ArrayList<>(), "Epic", "Description", Status.NEW, now, duration, endTime, TaskType.EPIC); + when(repository.findById(epicId)).thenReturn(Optional.of(epic)); + when(mapper.toResponseDto(epic)).thenReturn(dto); + + // Act + EpicResponseDto result = service.findById(epicId); + + // Assert + verify(repository, times(1)).findById(epicId); + verify(mapper, times(1)).toResponseDto(epic); + verify(history, times(1)).addToHistory(TaskType.EPIC, epicId); + assertEquals(dto, result); + assertEquals(endTime, result.endTime()); + assertEquals(TaskType.EPIC, result.type()); + } + + @Test + void findById_ShouldThrowNotFoundException_WhenEpicDoesNotExist() { + // Arrange + Long epicId = 1L; + when(repository.findById(epicId)).thenReturn(Optional.empty()); + + // Act & Assert + assertThrows(NotFoundException.class, () -> service.findById(epicId)); + verify(repository, times(1)).findById(epicId); + verify(mapper, never()).toResponseDto(any()); + verify(history, never()).addToHistory(any(), any()); + } + + // --- Tests for findAll method --- + + @Test + void findAll_ShouldReturnAllEpics() { + // Arrange + Epic epic1 = new Epic(); + epic1.setId(1L); + epic1.setName("Epic1"); + epic1.setDescription("Desc1"); + epic1.setStartTime(now); + epic1.setDuration(duration); + epic1.setEndTime(endTime); + epic1.setStatus(Status.NEW); + epic1.setSubtasks(new ArrayList<>()); + + Epic epic2 = new Epic(); + epic2.setId(2L); + epic2.setName("Epic2"); + epic2.setDescription("Desc2"); + epic2.setStartTime(now); + epic2.setDuration(duration); + epic2.setEndTime(endTime); + epic2.setStatus(Status.NEW); + epic2.setSubtasks(new ArrayList<>()); + + List epics = List.of(epic1, epic2); + EpicResponseDto dto1 = new EpicResponseDto(1L, new ArrayList<>(), "Epic1", "Desc1", Status.NEW, now, duration, endTime, TaskType.EPIC); + EpicResponseDto dto2 = new EpicResponseDto(2L, new ArrayList<>(), "Epic2", "Desc2", Status.NEW, now, duration, endTime, TaskType.EPIC); + when(repository.findAll()).thenReturn(epics); + when(mapper.toResponseDto(epic1)).thenReturn(dto1); + when(mapper.toResponseDto(epic2)).thenReturn(dto2); + + // Act + List result = service.findAll(); + + // Assert + verify(repository, times(1)).findAll(); + verify(mapper, times(2)).toResponseDto(any(Epic.class)); + assertEquals(2, result.size()); + assertTrue(result.contains(dto1)); + assertTrue(result.contains(dto2)); + } +} \ No newline at end of file diff --git a/service/src/test/java/service/task/manager/service/HistoryServiceImplTest.java b/service/src/test/java/service/task/manager/service/HistoryServiceImplTest.java new file mode 100644 index 0000000..5c80b96 --- /dev/null +++ b/service/src/test/java/service/task/manager/service/HistoryServiceImplTest.java @@ -0,0 +1,140 @@ +package service.task.manager.service; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.redis.core.ListOperations; +import org.springframework.data.redis.core.RedisTemplate; +import service.task.manager.model.HistoryEntry; +import service.task.manager.model.enums.TaskType; +import service.task.manager.service.impl.HistoryServiceImpl; + +import java.lang.reflect.Field; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class HistoryServiceImplTest { + + private static final String HISTORY_KEY = "history"; + private static final int HISTORY_SIZE = 10; + + @Mock + private RedisTemplate redisTemplate; + + @Mock + private ListOperations listOps; + + @InjectMocks + private HistoryServiceImpl historyService; + + @BeforeEach + void setUp() throws NoSuchFieldException, IllegalAccessException { + // Use reflection to inject listOps into historyService + Field listOpsField = HistoryServiceImpl.class.getDeclaredField("listOps"); + listOpsField.setAccessible(true); + listOpsField.set(historyService, listOps); + } + + @Test + void addToHistory_ShouldAddEntrySuccessfully() { + // Arrange + TaskType taskType = TaskType.TASK; + Long id = 1L; + when(listOps.rightPush(eq(HISTORY_KEY), any(HistoryEntry.class))).thenReturn(1L); + when(listOps.size(HISTORY_KEY)).thenReturn(1L); + + // Act + historyService.addToHistory(taskType, id); + + // Assert + verify(listOps, times(1)).rightPush(eq(HISTORY_KEY), any(HistoryEntry.class)); + verify(listOps, never()).leftPop(HISTORY_KEY); + } + + @Test + void addToHistory_ShouldRemoveOldestEntryWhenSizeExceedsLimit() { + // Arrange + TaskType taskType = TaskType.EPIC; + Long id = 2L; + when(listOps.rightPush(eq(HISTORY_KEY), any(HistoryEntry.class))).thenReturn(11L); + when(listOps.size(HISTORY_KEY)).thenReturn(11L); + when(listOps.leftPop(HISTORY_KEY)).thenReturn(new HistoryEntry(TaskType.TASK, 1L)); + + // Act + historyService.addToHistory(taskType, id); + + // Assert + verify(listOps, times(1)).rightPush(eq(HISTORY_KEY), any(HistoryEntry.class)); + verify(listOps, times(1)).leftPop(HISTORY_KEY); + } + + @Test + void addToHistory_ShouldHandleExceptionGracefully() { + // Arrange + TaskType taskType = TaskType.SUBTASK; + Long id = 3L; + when(listOps.rightPush(eq(HISTORY_KEY), any(HistoryEntry.class))) + .thenThrow(new RuntimeException("Redis exception")); + + // Act + historyService.addToHistory(taskType, id); + + // Assert + verify(listOps, times(1)).rightPush(eq(HISTORY_KEY), any(HistoryEntry.class)); + verify(listOps, never()).size(HISTORY_KEY); + verify(listOps, never()).leftPop(HISTORY_KEY); + } + + @Test + void getHistory_ShouldReturnHistoryEntries() { + // Arrange + List expectedHistory = List.of( + new HistoryEntry(TaskType.TASK, 1L), + new HistoryEntry(TaskType.EPIC, 2L) + ); + when(listOps.range(HISTORY_KEY, 0, -1)).thenReturn(expectedHistory); + + // Act + List actualHistory = historyService.getHistory(); + + // Assert + assertEquals(expectedHistory, actualHistory); + verify(listOps, times(1)).range(HISTORY_KEY, 0, -1); + } + + @Test + void getHistory_ShouldReturnEmptyListWhenHistoryIsNull() { + // Arrange + when(listOps.range(HISTORY_KEY, 0, -1)).thenReturn(null); + + // Act + List actualHistory = historyService.getHistory(); + + // Assert + assertNotNull(actualHistory); + assertTrue(actualHistory.isEmpty()); + verify(listOps, times(1)).range(HISTORY_KEY, 0, -1); + } + + @Test + void getHistory_ShouldReturnEmptyListOnException() { + // Arrange + when(listOps.range(HISTORY_KEY, 0, -1)).thenThrow(new RuntimeException("Redis exception")); + + // Act + List actualHistory = historyService.getHistory(); + + // Assert + assertNotNull(actualHistory); + assertTrue(actualHistory.isEmpty()); + verify(listOps, times(1)).range(HISTORY_KEY, 0, -1); + } +} \ No newline at end of file diff --git a/service/src/test/java/service/task/manager/service/SubtaskServiceImplTest.java b/service/src/test/java/service/task/manager/service/SubtaskServiceImplTest.java new file mode 100644 index 0000000..2e05b80 --- /dev/null +++ b/service/src/test/java/service/task/manager/service/SubtaskServiceImplTest.java @@ -0,0 +1,311 @@ +package service.task.manager.service; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import service.task.manager.dto.epic.EpicResponseDto; +import service.task.manager.dto.subtask.SubtaskRequestCreatedDto; +import service.task.manager.dto.subtask.SubtaskRequestUpdatedDto; +import service.task.manager.dto.subtask.SubtaskResponseDto; +import service.task.manager.exception.ConflictException; +import service.task.manager.exception.NotFoundException; +import service.task.manager.mapper.EpicMapper; +import service.task.manager.mapper.SubtaskMapper; +import service.task.manager.model.Epic; +import service.task.manager.model.Subtask; +import service.task.manager.model.enums.Status; +import service.task.manager.model.enums.TaskType; +import service.task.manager.repository.SubtaskRepository; +import service.task.manager.service.impl.SubtaskServiceImpl; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +/** + * Unit tests for SubtaskServiceImpl. + */ +@ExtendWith(MockitoExtension.class) +class SubtaskServiceImplTest { + + @Mock + private SubtaskRepository repository; + + @Mock + private EpicService epicService; + + @Mock + private SubtaskMapper mapper; + + @Mock + private EpicMapper epicMapper; + + @Mock + private HistoryService history; + + @InjectMocks + private SubtaskServiceImpl service; + + // Sample data for tests + private final LocalDateTime now = LocalDateTime.now(); + private final Duration duration = Duration.ofHours(24); + private final LocalDateTime endTime = now.plus(duration); + private final Long epicId = 1L; + private final Long subtaskId = 1L; + + // --- Tests for create method --- + + @Test + void create_ShouldCreateSubtask_WhenNameIsUniqueAndEpicExists() { + // Arrange + SubtaskRequestCreatedDto dto = new SubtaskRequestCreatedDto(epicId, "New Subtask", "Description", now, duration); + EpicResponseDto epicDto = new EpicResponseDto(epicId, new ArrayList<>(), "Epic", "Epic Description", Status.NEW, now, duration, endTime, TaskType.EPIC); + Epic epic = new Epic(); + epic.setId(epicId); + epic.setName("Epic"); + epic.setDescription("Epic Description"); + epic.setSubtasks(new ArrayList<>()); + + Subtask subtask = new Subtask(); + subtask.setName(dto.name()); + subtask.setDescription(dto.description()); + subtask.setStartTime(dto.startTime()); + subtask.setDuration(dto.duration()); + subtask.setEpic(epic); + + when(epicService.findById(epicId)).thenReturn(epicDto); + when(epicMapper.toEntity(epicDto)).thenReturn(epic); + when(repository.existsByName(dto.name())).thenReturn(false); + when(mapper.toEntity(dto)).thenReturn(subtask); + when(repository.save(any(Subtask.class))).thenReturn(subtask); + + // Act + service.create(dto); + + // Assert + verify(epicService, times(1)).findById(epicId); + verify(epicMapper, times(1)).toEntity(epicDto); + verify(repository, times(1)).existsByName(dto.name()); + verify(mapper, times(1)).toEntity(dto); + verify(repository, times(1)).save(subtask); + assertEquals(Status.NEW, subtask.getStatus()); + assertEquals(endTime, subtask.getEndTime()); + assertEquals(TaskType.SUBTASK, subtask.getType()); + assertEquals(epic, subtask.getEpic()); + } + + @Test + void create_ShouldThrowConflictException_WhenNameExists() { + // Arrange + SubtaskRequestCreatedDto dto = new SubtaskRequestCreatedDto(epicId, "Existing Subtask", "Description", now, duration); + EpicResponseDto epicDto = new EpicResponseDto(epicId, new ArrayList<>(), "Epic", "Epic Description", Status.NEW, now, duration, endTime, TaskType.EPIC); + + when(epicService.findById(epicId)).thenReturn(epicDto); + when(repository.existsByName(dto.name())).thenReturn(true); + + // Act & Assert + assertThrows(ConflictException.class, () -> service.create(dto)); + verify(epicService, times(1)).findById(epicId); + verify(repository, times(1)).existsByName(dto.name()); + verify(mapper, never()).toEntity((SubtaskRequestCreatedDto) any()); + verify(repository, never()).save(any()); + } + + @Test + void create_ShouldThrowNotFoundException_WhenEpicDoesNotExist() { + // Arrange + SubtaskRequestCreatedDto dto = new SubtaskRequestCreatedDto(epicId, "New Subtask", "Description", now, duration); + when(epicService.findById(epicId)).thenThrow(new NotFoundException("Epic with ID " + epicId + " not found")); + + // Act & Assert + assertThrows(NotFoundException.class, () -> service.create(dto)); + verify(epicService, times(1)).findById(epicId); + verify(repository, never()).existsByName(any()); + verify(mapper, never()).toEntity((SubtaskRequestCreatedDto) any()); + verify(repository, never()).save(any()); + } + + // --- Tests for update method --- + + @Test + void update_ShouldUpdateSubtask_WhenSubtaskExists() { + // Arrange + SubtaskRequestUpdatedDto dto = new SubtaskRequestUpdatedDto(subtaskId, "Updated Subtask", "Updated Description", Status.IN_PROGRESS, duration); + Subtask existingSubtask = new Subtask(); + existingSubtask.setId(subtaskId); + existingSubtask.setName("Old Subtask"); + existingSubtask.setDescription("Old Description"); + existingSubtask.setStartTime(now.minusDays(1)); + existingSubtask.setDuration(Duration.ofHours(12)); + existingSubtask.setStatus(Status.NEW); + + SubtaskResponseDto responseDto = new SubtaskResponseDto(subtaskId, epicId, "Updated Subtask", "Updated Description", Status.IN_PROGRESS, now.minusDays(1), now.minusDays(1).plus(duration), duration, TaskType.SUBTASK); + + when(repository.findById(subtaskId)).thenReturn(Optional.of(existingSubtask)); + when(mapper.toResponseDto(existingSubtask)).thenReturn(responseDto); + when(mapper.toEntity(responseDto)).thenReturn(existingSubtask); + when(repository.save(existingSubtask)).thenReturn(existingSubtask); + + // Act + SubtaskResponseDto result = service.update(dto); + + // Assert + verify(repository, times(1)).findById(subtaskId); + verify(mapper, times(1)).updateSubtaskFromDto(dto, existingSubtask); + verify(repository, times(1)).save(existingSubtask); + assertEquals("Updated Subtask", result.name()); + assertEquals(now.minusDays(1).plus(duration), result.endTime()); + assertEquals(TaskType.SUBTASK, result.type()); + } + + @Test + void update_ShouldThrowNotFoundException_WhenSubtaskDoesNotExist() { + // Arrange + SubtaskRequestUpdatedDto dto = new SubtaskRequestUpdatedDto(subtaskId, "Updated Subtask", "Updated Description", Status.IN_PROGRESS, duration); + when(repository.findById(subtaskId)).thenReturn(Optional.empty()); + + // Act & Assert + assertThrows(NotFoundException.class, () -> service.update(dto)); + verify(repository, times(1)).findById(subtaskId); + verify(mapper, never()).updateSubtaskFromDto(any(), any()); + verify(repository, never()).save(any()); + } + + // --- Tests for findById method --- + + @Test + void findById_ShouldReturnSubtask_WhenSubtaskExists() { + // Arrange + Subtask subtask = new Subtask(); + subtask.setId(subtaskId); + subtask.setName("Subtask"); + subtask.setDescription("Description"); + subtask.setStartTime(now); + subtask.setDuration(duration); + subtask.setEndTime(endTime); + subtask.setStatus(Status.NEW); + Epic epic = new Epic(); + epic.setId(epicId); + subtask.setEpic(epic); + + SubtaskResponseDto dto = new SubtaskResponseDto(subtaskId, epicId, "Subtask", "Description", Status.NEW, now, endTime, duration, TaskType.SUBTASK); + when(repository.findById(subtaskId)).thenReturn(Optional.of(subtask)); + when(mapper.toResponseDto(subtask)).thenReturn(dto); + + // Act + SubtaskResponseDto result = service.findById(subtaskId); + + // Assert + verify(repository, times(1)).findById(subtaskId); + verify(mapper, times(1)).toResponseDto(subtask); + verify(history, times(1)).addToHistory(TaskType.SUBTASK, subtaskId); + assertEquals(dto, result); + assertEquals(endTime, result.endTime()); + assertEquals(TaskType.SUBTASK, result.type()); + } + + @Test + void findById_ShouldThrowNotFoundException_WhenSubtaskDoesNotExist() { + // Arrange + when(repository.findById(subtaskId)).thenReturn(Optional.empty()); + + // Act & Assert + assertThrows(NotFoundException.class, () -> service.findById(subtaskId)); + verify(repository, times(1)).findById(subtaskId); + verify(mapper, never()).toResponseDto(any()); + verify(history, never()).addToHistory(any(), any()); + } + + // --- Tests for findAll method --- + + @Test + void findAll_ShouldReturnAllSubtasks() { + // Arrange + Subtask subtask1 = new Subtask(); + subtask1.setId(1L); + subtask1.setName("Subtask1"); + subtask1.setDescription("Desc1"); + subtask1.setStartTime(now); + subtask1.setDuration(duration); + subtask1.setEndTime(endTime); + subtask1.setStatus(Status.NEW); + Epic epic1 = new Epic(); + epic1.setId(epicId); + subtask1.setEpic(epic1); + + Subtask subtask2 = new Subtask(); + subtask2.setId(2L); + subtask2.setName("Subtask2"); + subtask2.setDescription("Desc2"); + subtask2.setStartTime(now); + subtask2.setDuration(duration); + subtask2.setEndTime(endTime); + subtask2.setStatus(Status.NEW); + subtask2.setEpic(epic1); + + List subtasks = List.of(subtask1, subtask2); + SubtaskResponseDto dto1 = new SubtaskResponseDto(1L, epicId, "Subtask1", "Desc1", Status.NEW, now, endTime, duration, TaskType.SUBTASK); + SubtaskResponseDto dto2 = new SubtaskResponseDto(2L, epicId, "Subtask2", "Desc2", Status.NEW, now, endTime, duration, TaskType.SUBTASK); + when(repository.findAll()).thenReturn(subtasks); + when(mapper.toResponseDto(subtask1)).thenReturn(dto1); + when(mapper.toResponseDto(subtask2)).thenReturn(dto2); + + // Act + List result = service.findAll(); + + // Assert + verify(repository, times(1)).findAll(); + verify(mapper, times(2)).toResponseDto(any(Subtask.class)); + assertEquals(2, result.size()); + assertTrue(result.contains(dto1)); + assertTrue(result.contains(dto2)); + } + + // --- Tests for delete method --- + + @Test + void delete_ShouldDeleteSubtask_WhenSubtaskExists() { + // Arrange + Subtask subtask = new Subtask(); + subtask.setId(subtaskId); + subtask.setName("Subtask"); + subtask.setDescription("Description"); + subtask.setStartTime(now); + subtask.setDuration(duration); + subtask.setEndTime(endTime); + subtask.setStatus(Status.NEW); + Epic epic = new Epic(); + epic.setId(epicId); + subtask.setEpic(epic); + + SubtaskResponseDto dto = new SubtaskResponseDto(subtaskId, epicId, "Subtask", "Description", Status.NEW, now, endTime, duration, TaskType.SUBTASK); + when(repository.findById(subtaskId)).thenReturn(Optional.of(subtask)); + when(mapper.toResponseDto(subtask)).thenReturn(dto); + + // Act + service.delete(subtaskId); + + // Assert + verify(repository, times(1)).findById(subtaskId); + verify(repository, times(1)).deleteById(subtaskId); + } + + @Test + void delete_ShouldThrowNotFoundException_WhenSubtaskDoesNotExist() { + // Arrange + when(repository.findById(subtaskId)).thenReturn(Optional.empty()); + + // Act & Assert + assertThrows(NotFoundException.class, () -> service.delete(subtaskId)); + verify(repository, times(1)).findById(subtaskId); + verify(repository, never()).deleteById(any()); + } +} diff --git a/service/src/test/java/service/task/manager/service/TaskServiceImplTest.java b/service/src/test/java/service/task/manager/service/TaskServiceImplTest.java new file mode 100644 index 0000000..ec998e1 --- /dev/null +++ b/service/src/test/java/service/task/manager/service/TaskServiceImplTest.java @@ -0,0 +1,265 @@ +package service.task.manager.service; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import service.task.manager.dto.task.TaskRequestCreatedDto; +import service.task.manager.dto.task.TaskRequestUpdatedDto; +import service.task.manager.dto.task.TaskResponseDto; +import service.task.manager.exception.ConflictException; +import service.task.manager.exception.NotFoundException; +import service.task.manager.mapper.TaskMapper; +import service.task.manager.model.Task; +import service.task.manager.model.enums.Status; +import service.task.manager.model.enums.TaskType; +import service.task.manager.repository.TaskRepository; +import service.task.manager.service.impl.TaskServiceImpl; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +/** + * Unit tests for TaskServiceImpl, aligned with updated DTOs and Task model. + */ +@ExtendWith(MockitoExtension.class) +class TaskServiceImplTest { + + @Mock + private TaskRepository repository; + + @Mock + private TaskMapper mapper; + + @Mock + private HistoryService history; + + @InjectMocks + private TaskServiceImpl taskService; + + // Sample data for tests + private final LocalDateTime now = LocalDateTime.now(); + private final Duration duration = Duration.ofHours(24); + private final LocalDateTime endTime = now.plus(duration); + + // --- Tests for create method --- + + @Test + void create_ShouldCreateTask_WhenNameIsUnique() { + // Arrange + TaskRequestCreatedDto dto = new TaskRequestCreatedDto("New Task", "Description", now, duration); + Task task = new Task(); + task.setName(dto.name()); + task.setDescription(dto.description()); + task.setStartTime(dto.startTime()); + task.setDuration(dto.duration()); + + when(repository.existsByName(dto.name())).thenReturn(false); + when(mapper.toEntity(dto)).thenReturn(task); + when(repository.save(any(Task.class))).thenReturn(task); + + // Act + taskService.create(dto); + + // Assert + verify(repository, times(1)).existsByName(dto.name()); + verify(mapper, times(1)).toEntity(dto); + verify(repository, times(1)).save(task); + assertEquals(Status.NEW, task.getStatus()); // Status set by addEndTimeTaskAndStatus + assertEquals(endTime, task.getEndTime()); // End time calculated + assertEquals(TaskType.TASK, task.getType()); // Default type + } + + @Test + void create_ShouldThrowConflictException_WhenNameExists() { + // Arrange + TaskRequestCreatedDto dto = new TaskRequestCreatedDto("Existing Task", "Description", now, duration); + when(repository.existsByName(dto.name())).thenReturn(true); + + // Act & Assert + assertThrows(ConflictException.class, () -> taskService.create(dto)); + verify(repository, times(1)).existsByName(dto.name()); + verify(mapper, never()).toEntity((TaskRequestCreatedDto) any()); + verify(repository, never()).save(any()); + } + + // --- Tests for update method --- + + @Test + void update_ShouldUpdateTask_WhenTaskExists() { + // Arrange + Long taskId = 1L; + TaskRequestUpdatedDto dto = new TaskRequestUpdatedDto(taskId, "Updated Task", "Updated Description", Status.IN_PROGRESS, duration); + Task existingTask = new Task(); + existingTask.setId(taskId); + existingTask.setName("Old Task"); + existingTask.setDescription("Old Description"); + existingTask.setStartTime(now.minusDays(1)); + existingTask.setDuration(Duration.ofHours(12)); + existingTask.setStatus(Status.NEW); + + TaskResponseDto responseDto = new TaskResponseDto(taskId, "Updated Task", "Updated Description", Status.IN_PROGRESS, now.minusDays(1), now.minusDays(1).plus(duration), duration, TaskType.TASK); + + when(repository.findById(taskId)).thenReturn(Optional.of(existingTask)); + when(mapper.toResponseDto(existingTask)).thenReturn(responseDto); + when(mapper.toEntity(responseDto)).thenReturn(existingTask); + when(repository.save(existingTask)).thenReturn(existingTask); + + // Act + TaskResponseDto result = taskService.update(dto); + + // Assert + verify(repository, times(1)).findById(taskId); // Called by findById internally + verify(mapper, times(1)).updateTaskFromDto(dto, existingTask); + verify(repository, times(1)).save(existingTask); + assertEquals("Updated Task", result.name()); + assertEquals(now.minusDays(1).plus(duration), result.endTime()); // End time recalculated + assertEquals(TaskType.TASK, result.type()); + } + + @Test + void update_ShouldThrowNotFoundException_WhenTaskDoesNotExist() { + // Arrange + Long taskId = 1L; + TaskRequestUpdatedDto dto = new TaskRequestUpdatedDto(taskId, "Updated Task", "Updated Description", Status.IN_PROGRESS, duration); + when(repository.findById(taskId)).thenReturn(Optional.empty()); + + // Act & Assert + assertThrows(NotFoundException.class, () -> taskService.update(dto)); + verify(repository, times(1)).findById(taskId); + verify(mapper, never()).updateTaskFromDto(any(), any()); + verify(repository, never()).save(any()); + } + + // --- Tests for findById method --- + + @Test + void findById_ShouldReturnTask_WhenTaskExists() { + // Arrange + Long taskId = 1L; + Task task = new Task(); + task.setId(taskId); + task.setName("Task"); + task.setDescription("Description"); + task.setStartTime(now); + task.setDuration(duration); + task.setEndTime(endTime); + task.setStatus(Status.NEW); + + TaskResponseDto dto = new TaskResponseDto(taskId, "Task", "Description", Status.NEW, now, endTime, duration, TaskType.TASK); + when(repository.findById(taskId)).thenReturn(Optional.of(task)); + when(mapper.toResponseDto(task)).thenReturn(dto); + + // Act + TaskResponseDto result = taskService.findById(taskId); + + // Assert + verify(repository, times(1)).findById(taskId); + verify(mapper, times(1)).toResponseDto(task); + verify(history, times(1)).addToHistory(TaskType.TASK, taskId); + assertEquals(dto, result); + assertEquals(endTime, result.endTime()); + assertEquals(TaskType.TASK, result.type()); + } + + @Test + void findById_ShouldThrowNotFoundException_WhenTaskDoesNotExist() { + // Arrange + Long taskId = 1L; + when(repository.findById(taskId)).thenReturn(Optional.empty()); + + // Act & Assert + assertThrows(NotFoundException.class, () -> taskService.findById(taskId)); + verify(repository, times(1)).findById(taskId); + verify(mapper, never()).toResponseDto(any()); + verify(history, never()).addToHistory(any(), any()); + } + + // --- Tests for findAll method --- + + @Test + void findAll_ShouldReturnAllTasks() { + // Arrange + Task task1 = new Task(); + task1.setId(1L); + task1.setName("Task1"); + task1.setDescription("Desc1"); + task1.setStartTime(now); + task1.setDuration(duration); + task1.setEndTime(endTime); + task1.setStatus(Status.NEW); + + Task task2 = new Task(); + task2.setId(2L); + task2.setName("Task2"); + task2.setDescription("Desc2"); + task2.setStartTime(now); + task2.setDuration(duration); + task2.setEndTime(endTime); + task2.setStatus(Status.NEW); + + List tasks = List.of(task1, task2); + TaskResponseDto dto1 = new TaskResponseDto(1L, "Task1", "Desc1", Status.NEW, now, endTime, duration, TaskType.TASK); + TaskResponseDto dto2 = new TaskResponseDto(2L, "Task2", "Desc2", Status.NEW, now, endTime, duration, TaskType.TASK); + when(repository.findAll()).thenReturn(tasks); + when(mapper.toResponseDto(task1)).thenReturn(dto1); + when(mapper.toResponseDto(task2)).thenReturn(dto2); + + // Act + List result = taskService.findAll(); + + // Assert + verify(repository, times(1)).findAll(); + verify(mapper, times(2)).toResponseDto(any(Task.class)); + assertEquals(2, result.size()); + assertTrue(result.contains(dto1)); + assertTrue(result.contains(dto2)); + } + + // --- Tests for delete method --- + + @Test + void delete_ShouldDeleteTask_WhenTaskExists() { + // Arrange + Long taskId = 1L; + Task task = new Task(); + task.setId(taskId); + task.setName("Task"); + task.setDescription("Description"); + task.setStartTime(now); + task.setDuration(duration); + task.setEndTime(endTime); + task.setStatus(Status.NEW); + + TaskResponseDto dto = new TaskResponseDto(taskId, "Task", "Description", + Status.NEW, now, endTime, duration, TaskType.TASK); + when(repository.findById(taskId)).thenReturn(Optional.of(task)); + when(mapper.toResponseDto(task)).thenReturn(dto); + + // Act + taskService.delete(taskId); + + // Assert + verify(repository, times(1)).findById(taskId); // Called by findById internally + verify(repository, times(1)).deleteById(taskId); + } + + @Test + void delete_ShouldThrowNotFoundException_WhenTaskDoesNotExist() { + // Arrange + Long taskId = 1L; + when(repository.findById(taskId)).thenReturn(Optional.empty()); + + // Act & Assert + assertThrows(NotFoundException.class, () -> taskService.delete(taskId)); + verify(repository, times(1)).findById(taskId); + verify(repository, never()).deleteById(any()); + } +} \ No newline at end of file diff --git a/src/task/manager/schedule/Main.java b/src/task/manager/schedule/Main.java deleted file mode 100644 index 68e87c4..0000000 --- a/src/task/manager/schedule/Main.java +++ /dev/null @@ -1,6 +0,0 @@ -package task.manager.schedule; - -public class Main { - public static void main(String[] args) { - } -} \ No newline at end of file diff --git a/src/task/manager/schedule/exception/ManagerSaveException.java b/src/task/manager/schedule/exception/ManagerSaveException.java deleted file mode 100644 index 338f552..0000000 --- a/src/task/manager/schedule/exception/ManagerSaveException.java +++ /dev/null @@ -1,18 +0,0 @@ -package task.manager.schedule.exception; - -public class ManagerSaveException extends RuntimeException { - public ManagerSaveException() { - } - - public ManagerSaveException(String message) { - super(message); - } - - public ManagerSaveException(String message, Throwable cause) { - super(message, cause); - } - - public ManagerSaveException(Throwable cause) { - super(cause); - } -} diff --git a/src/task/manager/schedule/exception/NotFoundException.java b/src/task/manager/schedule/exception/NotFoundException.java deleted file mode 100644 index ff8d235..0000000 --- a/src/task/manager/schedule/exception/NotFoundException.java +++ /dev/null @@ -1,18 +0,0 @@ -package task.manager.schedule.exception; - -public class NotFoundException extends RuntimeException { - public NotFoundException() { - } - - public NotFoundException(String message) { - super(message); - } - - public NotFoundException(String message, Throwable cause) { - super(message, cause); - } - - public NotFoundException(Throwable cause) { - super(cause); - } -} diff --git a/src/task/manager/schedule/model/Epic.java b/src/task/manager/schedule/model/Epic.java deleted file mode 100644 index 12233b1..0000000 --- a/src/task/manager/schedule/model/Epic.java +++ /dev/null @@ -1,52 +0,0 @@ -package task.manager.schedule.model; - -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; - -public class Epic extends Task { - - private final List subtaskIds = new ArrayList<>(); - private LocalDateTime endTime; - - public Epic(String name, Status status, String description, LocalDateTime startTime, long duration) { - super(name, status, description, startTime, duration); - } - - public Epic(int id, String name, String description, Status status, LocalDateTime startTime, long duration) { - super(name, status, description, startTime, duration); - setId(id); - } - - public void addSubtaskId(int id) { - subtaskIds.add(id); - } - - public void cleanSubtask() { - subtaskIds.clear(); - } - - public void removeSubtask(int id) { - subtaskIds.remove(id); - } - - public List getSubtaskIds() { - return subtaskIds; - } - - public TaskType getType() { - return TaskType.EPIC; - } - - public void setEndTime(LocalDateTime endTime) { - this.endTime = endTime; - } - - @Override - public String toString() { - return "Epic{" + - "subtaskIds=" + subtaskIds + - ", endTime=" + endTime + - '}'; - } -} diff --git a/src/task/manager/schedule/model/Subtask.java b/src/task/manager/schedule/model/Subtask.java deleted file mode 100644 index 0ff7bf7..0000000 --- a/src/task/manager/schedule/model/Subtask.java +++ /dev/null @@ -1,41 +0,0 @@ -package task.manager.schedule.model; - -import java.time.LocalDateTime; - -public class Subtask extends Task { - - private Integer epicId; - - public Subtask(String name, Status status, String description, LocalDateTime startTime, long duration, int epicId) { - - super(name, status, description, startTime, duration); - setEpicId(epicId); - } - - public Subtask(int id, String name, String description, Status status, LocalDateTime startTime, long duration, Integer epicId) { - super(name, status, description, startTime, duration); - setId(id); - setEpicId(epicId); - } - - @Override - public Integer getEpicId() { - return epicId; - } - - @Override - public TaskType getType() { - return TaskType.SUBTASK; - } - - public void setEpicId(Integer epicId) { - this.epicId = epicId; - } - - @Override - public String toString() { - return "Subtask{" + - "epicId=" + epicId + - '}'; - } -} diff --git a/src/task/manager/schedule/model/Task.java b/src/task/manager/schedule/model/Task.java deleted file mode 100644 index bcc4e8c..0000000 --- a/src/task/manager/schedule/model/Task.java +++ /dev/null @@ -1,118 +0,0 @@ -package task.manager.schedule.model; - -import java.time.Duration; -import java.time.LocalDateTime; -import java.util.Objects; - -public class Task { - private int id; - private String name; - private String description; - private Status status; - - private LocalDateTime startTime; // LocalDateTime - private Duration duration; // минуты или Duration - - public Task(String name, Status status, String description, LocalDateTime startTime, long duration) { - this.name = name; - this.status = status; - this.description = description; - this.startTime = LocalDateTime.from(startTime); - this.duration = Duration.ofMinutes(duration); - } - - public Task(int id, String name, String description, Status status, LocalDateTime startTime, long duration) { - setId(id); - this.name = name; - this.status = status; - this.description = description; - this.startTime = startTime; - this.duration = Duration.ofMinutes(duration); - } - - public int getId() { - return id; - } - - public void setId(int id) { - this.id = id; - } - - public String getName() { - return name; - } - - public Integer getEpicId() { - return null; - } - - public void setName(String name) { - this.name = name; - } - - public String getDescription() { - return description; - } - - public TaskType getType() { - return TaskType.TASK; - } - - public void setDescription(String description) { - this.description = description; - } - - public Status getStatus() { - return status; - } - - public void setStatus(Status status) { - this.status = status; - } - - public LocalDateTime getStartTime() { - return startTime; - } - - public void setStartTime(LocalDateTime startTime) { - this.startTime = startTime; - } - - public long getDuration() { - return duration.toMinutesPart(); - } - - public void setDuration(long duration) { - this.duration = Duration.ofMinutes(duration); - } - - public LocalDateTime getEndTime() { - return startTime.plus(duration); - } - - - @Override - public String toString() { - return "Task{" + - "id=" + id + - ", name='" + name + '\'' + - ", description='" + description + '\'' + - ", status=" + status + - ", startTime=" + startTime + - ", endTime=" + getEndTime() + - '}'; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Task task = (Task) o; - return id == task.id && Objects.equals(name, task.name) && Objects.equals(description, task.description) && status == task.status && Objects.equals(startTime, task.startTime) && Objects.equals(duration, task.duration); - } - - @Override - public int hashCode() { - return Objects.hash(id, name, description, status, startTime, duration); - } -} diff --git a/src/task/manager/schedule/server/Endpoint.java b/src/task/manager/schedule/server/Endpoint.java deleted file mode 100644 index dfce98e..0000000 --- a/src/task/manager/schedule/server/Endpoint.java +++ /dev/null @@ -1,19 +0,0 @@ -package task.manager.schedule.server; - -public enum Endpoint { - GET_TASKS, - GET_BY_ID, - GET_EPICS_ID_SUBTASKS, - GET_SUBTASKS, - GET_HISTORY, - GET_PRIORITIZED, - - - POST, - - - DELETE_BY_ID, - - - UNKNOWN -} diff --git a/src/task/manager/schedule/server/EndpointHandler.java b/src/task/manager/schedule/server/EndpointHandler.java deleted file mode 100644 index fc6ce12..0000000 --- a/src/task/manager/schedule/server/EndpointHandler.java +++ /dev/null @@ -1,39 +0,0 @@ -package task.manager.schedule.server; - -public class EndpointHandler { - public Endpoint getEndpoint(String requestPath, String requestMethod) { - if (requestPath.equals("/tasks") && requestMethod.equals("GET")) { - return Endpoint.GET_TASKS; - } else if (requestPath.equals("/tasks") && requestMethod.equals("POST")) { - return Endpoint.POST; - } else if (requestPath.equals("/tasks/" + requestPath.split("/")[2]) && requestMethod.equals("GET")) { - return Endpoint.GET_BY_ID; - } else if (requestPath.equals("/tasks/" + requestPath.split("/")[2]) && requestMethod.equals("DELETE")) { - return Endpoint.DELETE_BY_ID; - } else if (requestPath.equals("/epics") && requestMethod.equals("GET")) { - return Endpoint.GET_TASKS; - } else if (requestPath.equals("/epics") && requestMethod.equals("POST")) { - return Endpoint.POST; - } else if (requestPath.equals("epics/" + requestPath.split("/")[2]) && requestMethod.equals("GET")) { - return Endpoint.GET_BY_ID; - } else if (requestPath.equals("/epics/" + requestPath.split("/")[2]) && requestMethod.equals("DELETE")) { - return Endpoint.DELETE_BY_ID; - } else if (requestPath.equals("/epics/" + requestPath.split("/")[2] + "/subtasks") && requestMethod.equals("GET")) { - return Endpoint.GET_EPICS_ID_SUBTASKS; - } else if (requestPath.equals("/subtasks") && requestMethod.equals("GET")) { - return Endpoint.GET_SUBTASKS; - } else if (requestPath.equals("/subtasks") && requestMethod.equals("POST")) { - return Endpoint.POST; - } else if (requestPath.equals("/subtasks/" + requestPath.split("/")[2]) && requestMethod.equals("GET")) { - return Endpoint.GET_BY_ID; - } else if (requestPath.equals("subtasks/" + requestPath.split("/")[2]) && requestMethod.equals("DELETE")) { - return Endpoint.DELETE_BY_ID; - } else if (requestPath.endsWith("history") && requestMethod.equals("GET")) { - return Endpoint.GET_HISTORY; - } else if (requestPath.endsWith("prioritized") && requestMethod.equals("GET")) { - return Endpoint.GET_PRIORITIZED; - } else { - return Endpoint.UNKNOWN; - } - } -} diff --git a/src/task/manager/schedule/server/HttpHandler.java b/src/task/manager/schedule/server/HttpHandler.java deleted file mode 100644 index b46831d..0000000 --- a/src/task/manager/schedule/server/HttpHandler.java +++ /dev/null @@ -1,203 +0,0 @@ -package task.manager.schedule.server; - -import com.google.gson.*; -import com.sun.net.httpserver.HttpExchange; -import task.manager.schedule.exception.ManagerSaveException; -import task.manager.schedule.exception.NotFoundException; -import task.manager.schedule.model.Epic; -import task.manager.schedule.model.Subtask; -import task.manager.schedule.model.Task; -import task.manager.schedule.service.TaskManager; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; - -public class HttpHandler implements com.sun.net.httpserver.HttpHandler { - TaskManager manager; - TaskHandler taskHandler; - EndpointHandler endpointHandler; - public HttpHandler(TaskManager manager) { - this.manager = manager; - this.taskHandler = new TaskHandler(); - this.endpointHandler = new EndpointHandler(); - } - - @Override - public void handle(HttpExchange h) throws IOException { - GsonBuilder gsonBuilder = new GsonBuilder(); - gsonBuilder.setPrettyPrinting(); - Gson gson = gsonBuilder.create(); - Endpoint endpoint = endpointHandler.getEndpoint(h.getRequestURI().getPath(), h.getRequestMethod()); - final String path = h.getRequestURI().getPath(); - switch (endpoint) { - case GET_TASKS: - if (path.equals("/tasks")) { - sendText(h, gson.toJson(manager.getTasks().toString())); - break; - }else if(path.equals("/epics")) { - sendText(h, gson.toJson(manager.getEpics().toString())); - break; - } - case GET_BY_ID: - try { - int id = Integer.parseInt(h.getRequestURI().getPath().split("/")[2]); - if(path.equals("/tasks/"+ id)){ - sendText(h, gson.toJson(manager.getTask(id).toString())); - break; - }else if(path.equals("/epics/"+ id)){ - sendText(h, gson.toJson(manager.getEpic(id).toString())); - break; - }else if(path.equals("/subtasks/"+ id)){ - sendText(h, gson.toJson(manager.getSubtask(id).toString())); - break; - } - } catch (NotFoundException e) { - sendNotFound(h, "Данной задачи не существует по id: " + h.getRequestURI().getPath().split("/")[2]); - break; - } - case GET_EPICS_ID_SUBTASKS: - try { - sendText(h, gson.toJson(manager.getEpicSubtasks(Integer.parseInt(h.getRequestURI().getPath().split("/")[2])).toString())); - break; - } catch (NotFoundException e) { - sendNotFound(h, "Данного эпика не существует по id: " + e.getMessage()); - break; - } - case GET_HISTORY: - sendText(h, gson.toJson(manager.getHistory().toString())); - break; - case GET_PRIORITIZED: - sendText(h, gson.toJson(manager.getPrioritizedTasks().toString())); - break; - case POST: - convertTask(h); - break; - case DELETE_BY_ID: - try { - int id = Integer.parseInt(h.getRequestURI().getPath().split("/")[2]); - if(path.equals("/tasks/"+id)) { - manager.deleteTask(id); - sendText(h,""); - break; - } else if(path.equals("/epics/"+id)) { - manager.deleteEpic(id); - sendText(h,""); - break; - }else if(path.equals("/subtasks/"+id)) { - manager.deleteSubtask(id); - sendText(h,""); - break; - } - } catch (NotFoundException e) { - sendNotFound(h, "Данной задачи не существует по id: " + e.getMessage()); - break; - } - default: - errorServer(h); - break; - } - } - - protected void sendText(HttpExchange h, String text) throws IOException { - try (h) { - byte[] resp = text.getBytes(StandardCharsets.UTF_8); - h.getResponseHeaders().add("Content-Type", "application/json;charset=utf-8"); - h.sendResponseHeaders(200, resp.length); - h.getResponseBody().write(resp); - } - } - - protected void sendNotFound(HttpExchange h, String text) throws IOException { - try (h) { - byte[] resp = text.getBytes(StandardCharsets.UTF_8); - h.getResponseHeaders().add("Content-Type", "application/json;charset=utf-8"); - h.sendResponseHeaders(404, resp.length); - h.getResponseBody().write(resp); - } - } - - protected void updateText(HttpExchange h) throws IOException { - try (h) { - h.getResponseHeaders().add("Content-Type", "application/json;charset=utf-8"); - h.sendResponseHeaders(201, 0); - } - } - protected void errorServer(HttpExchange h) throws IOException { - try (h) { - h.getResponseHeaders().add("Content-Type", "application/json;charset=utf-8"); - h.sendResponseHeaders(500, 0); - } - } - - protected void sendHasInteractions(HttpExchange h, String text) throws IOException { - try (h) { - byte[] resp = text.getBytes(StandardCharsets.UTF_8); - h.getResponseHeaders().add("Content-Type", "application/json;charset=utf-8"); - h.sendResponseHeaders(406, resp.length); - h.getResponseBody().write(resp); - } - } - - - private void convertTask(HttpExchange h) throws IOException { - InputStream inputStream = h.getRequestBody(); - final String path = h.getRequestURI().getPath(); - final String[] body = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8).split(","); - Task task = taskHandler.taskFromString(body); - if (path.equals("/tasks")) { - if (body[0].split("=")[0].equals("id")) { - try { - manager.updateTask(task); - updateText(h); - } catch (ManagerSaveException e) { - sendHasInteractions(h, e.getMessage()); - } - } else { - try { - manager.createTask(task); - updateText(h); - } catch (ManagerSaveException e) { - sendHasInteractions(h, e.getMessage()); - } - } - } - if (path.equals("/epics")) { - if (body[0].split("=")[0].equals("id")) { - int taskId = Integer.parseInt(body[0].split("=")[1]); - try { - manager.updateEpic(new Epic(task.getId(), task.getName(), task.getDescription(), task.getStatus(), task.getStartTime(), task.getDuration())); - updateText(h); - } catch (ManagerSaveException e) { - sendHasInteractions(h, e.getMessage()); - } - } else { - try { - manager.createEpic(new Epic(task.getName(), task.getStatus(), task.getDescription(), task.getStartTime(), task.getDuration())); - updateText(h); - } catch (ManagerSaveException e) { - sendHasInteractions(h, e.getMessage()); - } - } - } - if (path.equals("/subtasks")) { - final int epicId = Integer.parseInt(body[6].split("=")[1]); - if (body[0].split("=")[0].equals("id")) { - int taskId = Integer.parseInt(body[0].split("=")[1]); - try { - manager.updateSubtask(new Subtask(task.getId(), task.getName(), task.getDescription(), task.getStatus(), task.getStartTime(), task.getDuration(), epicId)); - updateText(h); - } catch (ManagerSaveException e) { - sendHasInteractions(h, e.getMessage()); - } - } else { - try { - manager.createSubtask(new Subtask(task.getName(), task.getStatus(), task.getDescription(), task.getStartTime(), task.getDuration(), epicId)); - updateText(h); - } catch (ManagerSaveException e) { - sendHasInteractions(h, e.getMessage()); - } - } - } - } -} diff --git a/src/task/manager/schedule/server/HttpTaskServer.java b/src/task/manager/schedule/server/HttpTaskServer.java deleted file mode 100644 index 3dd0383..0000000 --- a/src/task/manager/schedule/server/HttpTaskServer.java +++ /dev/null @@ -1,33 +0,0 @@ -package task.manager.schedule.server; -import com.sun.net.httpserver.HttpServer; -import task.manager.schedule.model.Status; -import task.manager.schedule.model.Task; -import task.manager.schedule.service.Manager; -import task.manager.schedule.service.TaskManager; - -import java.io.IOException; -import java.net.InetSocketAddress; -import java.time.LocalDateTime; - -public class HttpTaskServer { - private static final int PORT = 8080; - public static void main(String[] args) throws IOException { - TaskManager taskManager = Manager.getDefault(); - - taskManager.createTask(new Task("Дом", Status.NEW, "Убраться в кухни и ванной", LocalDateTime.now(),40)); - taskManager.createTask(new Task("Работа", Status.IN_PROGRESS, "Сделать куча рутины и пойти домой:)",LocalDateTime.now().plusDays(1),50)); - - - - HttpHandler httpHandler = new HttpHandler(taskManager); - - HttpServer server = HttpServer.create(new InetSocketAddress(PORT), 0); - server.createContext("/tasks",httpHandler); - server.createContext("/epics",httpHandler); - server.createContext("/subtasks", httpHandler); - server.createContext("/history", httpHandler); - server.createContext("/prioritized", httpHandler); - server.start(); - System.out.println("HTTP-сервер запущен на " + PORT + " порту!"); - } -} diff --git a/src/task/manager/schedule/server/TaskHandler.java b/src/task/manager/schedule/server/TaskHandler.java deleted file mode 100644 index afc2f6d..0000000 --- a/src/task/manager/schedule/server/TaskHandler.java +++ /dev/null @@ -1,31 +0,0 @@ -package task.manager.schedule.server; - -import task.manager.schedule.model.Status; -import task.manager.schedule.model.Task; - -import java.time.Duration; -import java.time.LocalDateTime; - -public class TaskHandler { - public Task taskFromString(String[] body){ - if (body[0].split("=")[0].equals("id")) { - int taskId = Integer.parseInt(body[0].split("=")[1]); - final String name = body[1].split("=")[1]; - final String description = body[2].split("=")[1]; - final Status status = Status.valueOf(body[3].split("=")[1]); - final LocalDateTime startTime = LocalDateTime.parse(body[4].split("=")[1]); - final LocalDateTime endTime = LocalDateTime.parse(body[5].split("=")[1].replaceAll("}\"", "").trim()); - final Duration duration = Duration.between(startTime, endTime); - return new Task(taskId, name, description, status, startTime, duration.toMinutesPart()); - } else { - final String name = body[0].split("=")[1]; - final String description = body[1].split("=")[1]; - final Status status = Status.valueOf(body[2].split("=")[1]); - final LocalDateTime startTime = LocalDateTime.parse(body[3].split("=")[1]); - final LocalDateTime endTime = LocalDateTime.parse(body[4].split("=")[1].replaceAll("}\"", "").trim()); - final Duration duration = Duration.between(startTime, endTime); - return new Task(name, status, description, startTime, duration.toMinutesPart()); - } - } - -} diff --git a/src/task/manager/schedule/service/FileBackedTaskManager.java b/src/task/manager/schedule/service/FileBackedTaskManager.java deleted file mode 100644 index 861022e..0000000 --- a/src/task/manager/schedule/service/FileBackedTaskManager.java +++ /dev/null @@ -1,181 +0,0 @@ -package task.manager.schedule.service; - -import java.io.*; - -import java.nio.file.Files; - -import java.time.Duration; -import java.time.LocalDateTime; -import java.util.Map; - -import task.manager.schedule.exception.ManagerSaveException; -import ru.yandex.javacource.golotin.schedule.model.*; -import task.manager.schedule.service.inMemory.InMemoryTaskManager; -import task.manager.schedule.model.*; - -public class FileBackedTaskManager extends InMemoryTaskManager { - - private static final String HEADER = "id,type,name,status,description,epic"; - private final File file; - - public FileBackedTaskManager(File file) { - super(Manager.getDefaultHistory()); - this.file = file; - } - - public static FileBackedTaskManager loadFromFile(File file) { - final FileBackedTaskManager taskManager = new FileBackedTaskManager(file); - try { - final String csv = Files.readString(file.toPath()); - final String[] lines = csv.split(System.lineSeparator()); - int generatorId = 0; - for (int i = 1; i < lines.length; i++) { - String line = lines[i]; - if (line.isEmpty()) { - break; - } - final Task task = taskFromString(line); - final int id = task.getId(); - if (id > generatorId) { - generatorId = id; - } - if (task.getType() == TaskType.TASK) { - taskManager.createTask(task); - } else if (task.getType() == TaskType.SUBTASK) { - taskManager.createSubtask(new Subtask(task.getId(), task.getName(), task.getDescription(), - task.getStatus(), task.getStartTime(), task.getDuration(), task.getEpicId())); - } else if (task.getType() == TaskType.EPIC) { - taskManager.createEpic(new Epic(task.getId(), task.getName(), task.getDescription(), - task.getStatus(), task.getStartTime(), task.getDuration())); - for (Subtask subtask : taskManager.subtasks.values()) {// Поиск подзадач эпика - if (subtask.getEpicId() == task.getId()) { - Epic epic = taskManager.epics.get(task.getId()); - epic.addSubtaskId(subtask.getId()); - } - } - } - } - for (Map.Entry e : taskManager.subtasks.entrySet()) { - final Subtask subtask = e.getValue(); - final Epic epic = taskManager.epics.get(subtask.getEpicId()); - epic.addSubtaskId(subtask.getId()); - } - taskManager.counterId = generatorId; - } catch (IOException e) { - throw new ManagerSaveException("Невозможно прочитать файл: " + file.getName(), e); - } - return taskManager; - } - - @Override - public Task createTask(Task task) { - Task newTask = super.createTask(task); - saveToFile(); - return newTask; - } - - @Override - public Epic createEpic(Epic epic) { - Epic newEpic = super.createEpic(epic); - saveToFile(); - return newEpic; - } - - @Override - public Subtask createSubtask(Subtask subtask) { - Subtask newSubtask = super.createSubtask(subtask); - saveToFile(); - return newSubtask; - } - - @Override - public void updateTask(Task task) { - super.updateTask(task); - saveToFile(); - } - - @Override - public void updateEpic(Epic epic) { - super.updateEpic(epic); - saveToFile(); - } - - @Override - public void updateSubtask(Subtask subTask) { - super.updateSubtask(subTask); - saveToFile(); - } - - @Override - public void deleteTask(int id) { - super.deleteTask(id); - saveToFile(); - } - - @Override - public void deleteEpic(int id) { - super.deleteEpic(id); - saveToFile(); - } - - @Override - public void deleteSubtask(int id) { - super.deleteSubtask(id); - saveToFile(); - } - - public static String toString(Task task) { - return task.getId() + "," + task.getType() + "," + task.getName() + "," + task.getStatus() + "," + - task.getDescription() + "," + (task.getType().equals(TaskType.SUBTASK) ? task.getEpicId() : ""+task.getStartTime()+","+task.getEndTime()); - } - - - public static Task taskFromString(String value) { - final String[] values = value.split(","); - final int id = Integer.parseInt(values[0]); - final TaskType type = TaskType.valueOf(values[1]); - final String name = values[2]; - final Status status = Status.valueOf(values[3]); - final String description = values[4]; - final LocalDateTime startTime = LocalDateTime.parse(values[5]); - final Duration duration = Duration.between(LocalDateTime.parse(values[5]), LocalDateTime.parse(values[6])); - if (type == TaskType.TASK) { - return new Task(id, name, description, status, startTime, duration.toMinutesPart()); - } - if (type == TaskType.SUBTASK) { - final int epicId = Integer.parseInt(values[7]); - return new Subtask(id, name, description, status, startTime, duration.toMinutesPart(), epicId); - } - - return new Epic(id, name, description, status, startTime, duration.toMinutesPart()); - } - - protected void saveToFile() { - try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) { - writer.write(HEADER); - writer.newLine(); - - for (Map.Entry entry : tasks.entrySet()) { - final Task task = entry.getValue(); - writer.write(toString(task)); - writer.newLine(); - } - - for (Map.Entry entry : subtasks.entrySet()) { - final Task task = entry.getValue(); - writer.write(toString(task)); - writer.newLine(); - } - - for (Map.Entry entry : epics.entrySet()) { - final Task task = entry.getValue(); - writer.write(toString(task)); - writer.newLine(); - } - - writer.newLine(); - } catch (IOException e) { - throw new ManagerSaveException("Ошибка сохранения файла: " + file.getName(), e); - } - } -} diff --git a/src/task/manager/schedule/service/HistoryManager.java b/src/task/manager/schedule/service/HistoryManager.java deleted file mode 100644 index 9b81442..0000000 --- a/src/task/manager/schedule/service/HistoryManager.java +++ /dev/null @@ -1,15 +0,0 @@ -package task.manager.schedule.service; - -import java.util.List; - -import task.manager.schedule.model.Task; - -public interface HistoryManager { - void add(Task task); - - List getAll(); - - void remove(int id); - - -} \ No newline at end of file diff --git a/src/task/manager/schedule/service/Manager.java b/src/task/manager/schedule/service/Manager.java deleted file mode 100644 index a14dbc8..0000000 --- a/src/task/manager/schedule/service/Manager.java +++ /dev/null @@ -1,15 +0,0 @@ -package task.manager.schedule.service; - -import task.manager.schedule.service.inMemory.InMemoryHistoryManager; - -import java.io.File; - -public class Manager { - public static TaskManager getDefault() { - return new FileBackedTaskManager(new File("resources/task.csv")); - } - - public static InMemoryHistoryManager getDefaultHistory() { - return new InMemoryHistoryManager(); - } -} diff --git a/src/task/manager/schedule/service/TaskManager.java b/src/task/manager/schedule/service/TaskManager.java deleted file mode 100644 index 2b6fc36..0000000 --- a/src/task/manager/schedule/service/TaskManager.java +++ /dev/null @@ -1,49 +0,0 @@ -package task.manager.schedule.service; - -import task.manager.schedule.model.Epic; -import task.manager.schedule.model.Subtask; -import task.manager.schedule.model.Task; - -import java.util.List; - -public interface TaskManager { - Task createTask(Task task); - - Task createEpic(Epic epic); - - Subtask createSubtask(Subtask subtask); - - void updateTask(Task task); - - void updateEpic(Epic epic); - - void updateSubtask(Subtask subtask); - - void cleanTasks(); - - void cleanSubtasks(); - - void cleanEpics(); - - void deleteTask(int id); - - void deleteSubtask(int id); - - void deleteEpic(int id); - - List getTasks(); - - List getEpics(); - - List getEpicSubtasks(int epicId); - - Task getTask(int id); - - Epic getEpic(int id); - - Subtask getSubtask(int id); - - List getHistory(); - - List getPrioritizedTasks(); -} diff --git a/src/task/manager/schedule/service/inMemory/InMemoryHistoryManager.java b/src/task/manager/schedule/service/inMemory/InMemoryHistoryManager.java deleted file mode 100644 index 6db73c0..0000000 --- a/src/task/manager/schedule/service/inMemory/InMemoryHistoryManager.java +++ /dev/null @@ -1,92 +0,0 @@ -package task.manager.schedule.service.inMemory; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import task.manager.schedule.model.Task; -import task.manager.schedule.service.HistoryManager; - -public class InMemoryHistoryManager implements HistoryManager { - - private static class Node { - Task item; - Node next; - Node prev; - - Node(Node prev, Task element, Node next) {// присвоение prev, element, next - this.item = element; - this.next = next; - this.prev = prev; - } - } - - private static final Map history = new HashMap<>(); - private Node first; - private Node last; - - @Override - public void add(Task task) {// добавление Task - - if (task == null) { - return; - }else { - remove(task.getId()); - linkLast(task); - history.put(task.getId(), last); - - } - } - - @Override - public List getAll() {// вывод списка истории - List list = new ArrayList<>(); - Node current = first; - while (current != null) { - - current = current.next; - } - return list; - } - - - @Override - public void remove(int id) {// удаление по id - final Node node = history.remove(id); - if (node == null) { - return; - } - removeNode(node); - } - - private void removeNode(Node node) { - - if (node.prev != null) { - node.prev.next = node.next; - if (node.next == null) { - last = node.prev; - } else { - node.next.prev = node.prev; - } - } else { - first = node.next; - if (first == null) { - last = null; - } else { - first.prev = null; - } - } - } - - private void linkLast(Task task) {// двигаем историю - final Node lastLink = last; - final Node newNode = new Node(lastLink, task, null); - last = newNode; - if (lastLink == null) { - first = newNode; - } else { - lastLink.next = newNode; - } - } -} \ No newline at end of file diff --git a/src/task/manager/schedule/service/inMemory/InMemoryTaskManager.java b/src/task/manager/schedule/service/inMemory/InMemoryTaskManager.java deleted file mode 100644 index 4b6b71b..0000000 --- a/src/task/manager/schedule/service/inMemory/InMemoryTaskManager.java +++ /dev/null @@ -1,295 +0,0 @@ -package task.manager.schedule.service.inMemory; - -import task.manager.schedule.exception.ManagerSaveException; -import task.manager.schedule.exception.NotFoundException; -import task.manager.schedule.model.Epic; -import task.manager.schedule.model.Status; -import task.manager.schedule.model.Subtask; -import task.manager.schedule.model.Task; -import task.manager.schedule.service.HistoryManager; -import task.manager.schedule.service.TaskManager; - -import java.time.LocalDateTime; -import java.util.*; -import java.util.stream.Collectors; - - -public class InMemoryTaskManager implements TaskManager { - protected int counterId = 0; - protected final Map tasks; - protected final Map epics; - protected final Map subtasks; - protected final HistoryManager historyManager; - protected final Set prioritizedTasks; - - public InMemoryTaskManager(HistoryManager historyManager) { - this.historyManager = historyManager; // 3 - this.tasks = new HashMap<>(); - this.epics = new HashMap<>(); - this.subtasks = new HashMap<>(); - this.prioritizedTasks = new TreeSet<>(Comparator.comparing(Task::getStartTime)); - } - - @Override - public Task createTask(Task task) {// создание Task - final int id = ++counterId; - task.setId(id); - addPriorityTask(task); - tasks.put(id, task); - return task; - } - - @Override - public Epic createEpic(Epic epic) {// создание Epic - final int id = ++counterId; - epic.setId(id); - addPriorityTask(epic); - epics.put(id, epic); - return epic; - } - - - @Override - public Subtask createSubtask(Subtask subtask) {// создание Subtask - final int epicId = subtask.getEpicId(); - Epic epic = epics.get(epicId); - if (epic == null) { - return null; - } - updateEpicDuration(epic); - final int id = ++counterId; - subtask.setId(id); - addPriorityTask(subtask); - subtasks.put(id, subtask); - epic.addSubtaskId(subtask.getId()); - updateEpicStatus(epicId); - return subtask; - } - - @Override - public void updateTask(Task task) {// обновление Task - final Task savedTask = tasks.get(task.getId()); - if (savedTask == null) { - return; - } - addPriorityTask(task); - tasks.put(task.getId(), task); - } - - @Override - public void updateEpic(Epic epic) {// обновление Epic - final Epic savedEpic = epics.get(epic.getId()); - if (savedEpic == null) { - return; - } - savedEpic.setName(epic.getName()); - savedEpic.setDescription(epic.getDescription()); - addPriorityTask(savedEpic); - epics.put(epic.getId(), epic); - } - - @Override - public void updateSubtask(Subtask subtask) {// обновление Subtask - final int epicId = subtask.getEpicId(); - final Subtask savedSubtask = subtasks.get(subtask.getId()); - if (savedSubtask == null) { - return; - } - final Epic epic = epics.get(epicId); - if (epic == null) { - return; - } - addPriorityTask(savedSubtask); - subtasks.put(subtask.getId(), subtask); - updateEpic(epicId);// обновление статуса у Epic - } - - @Override - public void cleanTasks() { - tasks.clear(); - }// очистка списка Tasks - - public void cleanSubtasks() {// очистка списка Subtasks - for (Epic epic : epics.values()) { - epic.cleanSubtask(); - updateEpicStatus(epic.getId()); - } - subtasks.clear(); - } - - @Override - public void cleanEpics() {// очистка списка Epics и Subtasks - epics.clear(); - subtasks.clear(); - - } - - @Override - public void deleteTask(int id) { - tasks.remove(id); - }// удаление по id Task - - @Override - public void deleteSubtask(int id) {// удаление по id Subtask - Subtask subtask = subtasks.remove(id); - if (subtask == null) { - return; - } - Epic epic = epics.get(subtask.getEpicId()); - epic.removeSubtask(id); - updateEpicStatus(epic.getId()); - } - - @Override - public void deleteEpic(int id) {// удаление по id Epic - Epic epic = epics.remove(id); - if (epic == null) { - return; - } - for (Integer subtaskId : epic.getSubtaskIds()) { - subtasks.remove(subtaskId); - } - } - - @Override - public List getTasks() { - return new ArrayList<>(tasks.values()); - }// получаем список Tasks - - @Override - public List getEpics() { - return new ArrayList<>(epics.values()); - }// получаем список Epics - - @Override - public List getEpicSubtasks(int epicId) {// получаем список Epic с Subtasks - final Epic epic = epics.get(epicId); - if (epic == null) { - throw new NotFoundException("Задача с ид=" + epicId); - } - updateEpic(epicId); - return epic.getSubtaskIds().stream().map(subtasks::get).collect(Collectors.toList()); - } - - @Override - public Task getTask(int id) {// получаем Task по id - final Task task = tasks.get(id); - if (task == null) { - throw new NotFoundException("Задача с ид=" + id); - } - historyManager.add(task); - return task; - - } - - @Override - public Epic getEpic(int id) {// получаем Epic по id - final Epic epic = epics.get(id); - if (epic == null) { - throw new NotFoundException("Эпик с ид=" + id); - } - historyManager.add(epic); - return epic; - } - - @Override - public Subtask getSubtask(int id) {// получаем Subtask по id - final Subtask subtask = subtasks.get(id); - if (subtask == null) { - throw new NotFoundException("Подзадача с ид=" + id); - } - historyManager.add(subtask); - return subtask; - } - - @Override - public List getHistory() {// получаем список истории - return historyManager.getAll(); - } - - private void updateEpicStatus(int epicId) {// обновление статуса Epic - Epic epic = epics.get(epicId); - List subtasks = epic.getSubtaskIds().stream() - .filter(this.subtasks::containsKey) - .map(this.subtasks::get) - .toList(); - for (Subtask statusSubtask : subtasks) { - short subtaskNew = 0; - short subtaskDone = 0; - if (statusSubtask.getStatus() == Status.IN_PROGRESS) { - epic.setStatus(Status.IN_PROGRESS); - break; - } else if (statusSubtask.getStatus() == Status.NEW) { - subtaskNew++; - } else if (statusSubtask.getStatus() == Status.DONE) { - subtaskDone++; - } - if (subtaskDone == subtasks.size()) { - epic.setStatus(Status.DONE); - break; - } - if (subtaskNew == subtasks.size()) { - epic.setStatus(Status.NEW); - } else { - epic.setStatus(Status.IN_PROGRESS); - } - break; - } - } - - private void updateEpicDuration(Epic epic) { - List subs = epic.getSubtaskIds(); - if (subs.isEmpty()) { - epic.setDuration(0L); - return; - } - LocalDateTime start = LocalDateTime.MAX; - LocalDateTime end = LocalDateTime.MIN; - long duration = 0L; - for (int id : subs) { - final Subtask subtask = subtasks.get(id); - final LocalDateTime startTime = subtask.getStartTime(); - final LocalDateTime endTime = subtask.getEndTime(); - if (startTime.isBefore(start)) { - start = startTime; - } - if (endTime.isAfter(end)) { - end = endTime; - } - duration += subtask.getDuration(); - } - epic.setDuration(duration); - epic.setStartTime(start); - epic.setEndTime(end); - } - - protected void updateEpic(int epicId) { - Epic epic = epics.get(epicId); - updateEpicStatus(epicId); - updateEpicDuration(epic); - } - - private void addPriorityTask(Task task) { - final LocalDateTime startTime = task.getStartTime(); - final LocalDateTime endTime = task.getEndTime(); - for (Task t : prioritizedTasks) { - final LocalDateTime existStart = t.getStartTime(); - final LocalDateTime existEnd = t.getEndTime(); - if (!endTime.isAfter(existStart)) { - continue; - } - if (!existEnd.isAfter(startTime)) { - continue; - } - - throw new ManagerSaveException("Задача пересекаются с id=" + t.getId() + " c " + existStart + " по " + existEnd); - } - - prioritizedTasks.add(task); - } - - @Override - public List getPrioritizedTasks() { - return new ArrayList<>(prioritizedTasks); - } -} diff --git a/test/task/manager/schedule/module/EpicTest.java b/test/task/manager/schedule/module/EpicTest.java deleted file mode 100644 index 4a159da..0000000 --- a/test/task/manager/schedule/module/EpicTest.java +++ /dev/null @@ -1,19 +0,0 @@ -package ru.yandex.javacource.golotin.schedule.test.model; - -import org.junit.jupiter.api.Test; -import ru.yandex.javacource.golotin.schedule.model.Epic; -import ru.yandex.javacource.golotin.schedule.model.Status; - -import static org.junit.jupiter.api.Assertions.*; - -class EpicTest { - - @Test - void shouldEqualsWithCopy() { - Epic epic = new Epic("name", Status.NEW, "desc"); - Epic epicExpected = new Epic("name1", Status.NEW, "desc"); - assertEquals(epicExpected, epic); - - } - -} \ No newline at end of file diff --git a/test/task/manager/schedule/service/FileBackedTaskManagerTest.java b/test/task/manager/schedule/service/FileBackedTaskManagerTest.java deleted file mode 100644 index b691320..0000000 --- a/test/task/manager/schedule/service/FileBackedTaskManagerTest.java +++ /dev/null @@ -1,49 +0,0 @@ -package ru.yandex.javacource.golotin.schedule.service; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import ru.yandex.javacource.golotin.schedule.model.Status; -import ru.yandex.javacource.golotin.schedule.model.Task; - -import java.io.File; -import java.time.LocalDateTime; - -import static org.junit.jupiter.api.Assertions.*; - -class FileBackedTaskManagerTest { - TaskManager taskManager; - @BeforeEach - void beforeEach() { - taskManager = Manager.getDefault(); - - } - - @Test - void createTask() { - LocalDateTime localDateTime = LocalDateTime.now().plusDays(1); - Task task = new Task("test", Status.NEW, "testing", localDateTime,30); - Task task2 = new Task("test2", Status.NEW, "testing2",LocalDateTime.now(),45); - taskManager.createTask(task); - taskManager.createTask(task2); - assertEquals(taskManager.getTasks(),FileBackedTaskManager.loadFromFile(new File("resources/task.csv")).getTasks()); - } - - @Test - void updateTask() { - Task task = new Task("test", Status.NEW, "testing", LocalDateTime.now(),30); - taskManager.createTask(task); - Task task2 = new Task(0,"test2","testing2", Status.NEW, LocalDateTime.now(),45); - taskManager.updateTask(task2); - assertEquals(task, FileBackedTaskManager.loadFromFile(new File("resources/task.csv")).getTask(1)); - } - - @Test - void deleteTask() { - Task task = new Task("test", Status.NEW, "testing", LocalDateTime.now(),30); - taskManager.createTask(task); - TaskManager taskManager1 = taskManager; - taskManager.deleteTask(task.getId()); - assertNotEquals(taskManager, taskManager1); - } - -} \ No newline at end of file diff --git a/test/task/manager/schedule/service/InMemoryTaskManagerTest.java b/test/task/manager/schedule/service/InMemoryTaskManagerTest.java deleted file mode 100644 index 245f262..0000000 --- a/test/task/manager/schedule/service/InMemoryTaskManagerTest.java +++ /dev/null @@ -1,110 +0,0 @@ -package ru.yandex.javacource.golotin.schedule.test.service; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import ru.yandex.javacource.golotin.schedule.exception.ManagerSaveException; -import ru.yandex.javacource.golotin.schedule.model.Status; -import ru.yandex.javacource.golotin.schedule.model.Task; -import ru.yandex.javacource.golotin.schedule.service.InMemoryTaskManager; -import ru.yandex.javacource.golotin.schedule.service.Manager; -import ru.yandex.javacource.golotin.schedule.service.TaskManager; - - -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; - -class InMemoryTaskManagerTest { - TaskManager taskManager; - - @BeforeEach - void beforeEach() { - taskManager = Manager.getDefault(); - } - - - @Test - void shouldCreateTask() { - LocalDateTime localDateTime = LocalDateTime.now().plusDays(1); - Task task = new Task("test", Status.NEW, "testing", localDateTime,30); - Task task2 = new Task("test2", Status.NEW, "testing2",LocalDateTime.now(),45); - Task task3 = task; - assertEquals(task, task2); - assertSame(task, task3); - - Task result = taskManager.createTask(task); - - assertNotNull(result); - Task clone = taskManager.getTask(result.getId()); - assertEquals(clone.getId(), result.getId()); - assertEquals(clone.getName(), result.getName()); - assertTrue(taskManager.getTasks().contains(task)); - - } - - @Test - void shouldUpdateTask() { - LocalDateTime localDateTime = LocalDateTime.now().plusDays(1); - Task task = new Task("test", Status.NEW, "testing", localDateTime,30); - taskManager.createTask(task); - Task task2 = new Task(0,"test2", "testing2", Status.NEW, LocalDateTime.now(),45); - taskManager.updateTask(task2); - assertEquals(task, taskManager.getTask(1)); - } - - @Test - void shouldDeleteTask() { - LocalDateTime localDateTime = LocalDateTime.now().plusDays(1); - Task task = new Task("test", Status.NEW, "testing", localDateTime,30); - Task task2 = new Task("test2", Status.NEW, "testing2", LocalDateTime.now(),45); - taskManager.createTask(task); - taskManager.createTask(task2); - taskManager.deleteTask(1); - Exception exception = assertThrows(ManagerSaveException.class, ()->taskManager.getTask(1)); - String expectedMessage = "Задача с ид=" + 1; - String actualMessage = exception.getMessage(); - assertTrue(actualMessage.contains(expectedMessage)); - - } - - @Test - void shouldCleanTask() { - LocalDateTime localDateTime = LocalDateTime.now().plusDays(1); - Task task = new Task("test", Status.NEW, "testing", LocalDateTime.now(),30); - Task task2 = new Task("test2", Status.NEW, "testing2", localDateTime,45); - taskManager.createTask(task); - taskManager.createTask(task2); - taskManager.cleanTasks(); - assertEquals(taskManager.getTasks(), taskManager.getEpics()); - } - - @Test - void shouldGetTasks() { - LocalDateTime localDateTime = LocalDateTime.now().plusDays(1); - Task task = new Task("test", Status.NEW, "testing", localDateTime,30); - Task task2 = new Task("test2", Status.NEW, "testing2", LocalDateTime.now(),45); - taskManager.createTask(task); - taskManager.createTask(task2); - - List tasks = new ArrayList<>(); - tasks.add(task); - tasks.add(task2); - assertEquals(tasks, taskManager.getTasks()); - } - - @Test - void shouldaddPriorityTask(){ - LocalDateTime localDateTime = LocalDateTime.now(); - Task task = new Task("test", Status.NEW, "testing", localDateTime,30); - Task task2 = new Task("test2", Status.NEW, "testing2", localDateTime,45); - taskManager.createTask(task); - Exception exception = assertThrows(ManagerSaveException.class, () -> taskManager.createTask(task2)); - - String expectedMessage = "Задача пересекаются с id=" + task.getId() + " c " + task.getStartTime() + " по " + task.getEndTime(); - String actualMessage = exception.getMessage(); - assertTrue(actualMessage.contains(expectedMessage)); - - } -} \ No newline at end of file