diff --git a/.env b/.env
new file mode 100644
index 00000000..1e97dd34
--- /dev/null
+++ b/.env
@@ -0,0 +1,5 @@
+BASEPROJECT_DATABASE_HOST=postgres
+BASEPROJECT_DATABASE_PORT=5434
+BASEPROJECT_DATABASE_NAME=mynewdatabase
+BASEPROJECT_DATABASE_USERNAME=postgres
+BASEPROJECT_DATABASE_PASSWORD=root
\ No newline at end of file
diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties
new file mode 100644
index 00000000..d58dfb70
--- /dev/null
+++ b/.mvn/wrapper/maven-wrapper.properties
@@ -0,0 +1,19 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+wrapperVersion=3.3.2
+distributionType=only-script
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 00000000..88b28fbc
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,20 @@
+FROM bellsoft/liberica-openjdk-alpine:22 AS builder
+WORKDIR /application
+COPY . .
+RUN --mount=type=cache,target=/root/.m2 chmod +x mvnw && ./mvnw clean install -Dmaven.test.skip
+
+FROM bellsoft/liberica-openjre-alpine:22 AS layers
+WORKDIR /application
+COPY --from=builder /application/target/*.jar app.jar
+RUN java -Djarmode=layertools -jar app.jar extract
+
+FROM bellsoft/liberica-openjre-alpine:22
+VOLUME /tmp
+RUN adduser -S spring-user
+USER spring-user
+COPY --from=layers /application/dependencies/ ./
+COPY --from=layers /application/spring-boot-loader/ ./
+COPY --from=layers /application/snapshot-dependencies/ ./
+COPY --from=layers /application/application/ ./
+
+ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]
diff --git a/README.md b/README.md
index 60f40028..507b2a95 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,7 @@
# Практическое задание 1 # Spring ToDo
-Создайте простое RESTful API веб-сервиса с использованием Spring Boot. Должно быть разработано TODO приложение, которое позволяет вести список дел
+Создайте простое RESTful API веб-сервиса с использованием Spring Boot. Должно быть разработано TODO приложение,
+которое позволяет вести список дел
**Описание:**
- [x] В качестве БД используйте PostgreSQL
@@ -18,3 +19,22 @@
3. После выполнения работы открыть Pull Request в master **!!!СВОЕГО РЕПОЗИТОРИЯ!!!**
4. Назначить ментора ревьювером
5. Отправить ссылку на PR в тг чат ментору
+
+------------------------------------------------------------------------------------------------
+
+Swagger находится по адресу - http://localhost:8081/swagger-ui/index.html#
+PgAdmin находится по адресу - http://localhost:5050/, логин admin@admin.com, пароль root
+
+Для того, чтобы поднять проект в докере(редис, пгадмин, приложение, БД), необходимо установить переменные среды на ПК, в
+*Переменных среды для пользователя <имя пользователя>*
+
+* Имя переменной: `BASEPROJECT_DATABASE_USERNAME`
+ Значение: `postgres`
+* Имя переменной: `BASEPROJECT_DATABASE_PASSWORD`
+ Значение `root`
+* Имя переменной: `BASEPROJECT_DATABASE_PORT`
+ Значение: `5434`(если порт свободен)
+* Имя переменной: `BASEPROJECT_DATABASE_NAME`
+ Значение: `mynewdatabase`
+* Имя переменной: `BASEPROJECT_DATABASE_HOST`
+ Значение: `localhost`
\ No newline at end of file
diff --git a/docker-compose.yaml b/docker-compose.yaml
new file mode 100644
index 00000000..c9f4e88b
--- /dev/null
+++ b/docker-compose.yaml
@@ -0,0 +1,64 @@
+services:
+ postgres:
+ image: postgres:17.2
+ restart: "no"
+ ports:
+ - "5434:5432"
+ volumes:
+ - postgres_data:/var/lib/postgresql/data
+ environment:
+ POSTGRES_USER: ${BASEPROJECT_DATABASE_USERNAME}
+ POSTGRES_PASSWORD: ${BASEPROJECT_DATABASE_PASSWORD}
+ POSTGRES_DB: ${BASEPROJECT_DATABASE_NAME}
+ healthcheck:
+ test: pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB
+ interval: 10s
+ timeout: 5s
+ start_period: 10s
+ retries: 5
+
+ pgadmin:
+ image: dpage/pgadmin4:latest
+ restart: "no"
+ depends_on:
+ - postgres
+ ports:
+ - "5050:80" # внешний порт 5050 -> внутренний 80 (интерфейс pgAdmin)
+ environment:
+ PGADMIN_DEFAULT_EMAIL: "admin@admin.com" # любой логин
+ PGADMIN_DEFAULT_PASSWORD: "root" # любой пароль
+
+ spring-to-do:
+ image: spring-to-do:latest
+ build:
+ context: .
+ args:
+ DOCKER_BUILDKIT: 1
+ restart: "no"
+ ports:
+ - "8081:8080"
+ environment:
+ SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/mynewdatabase
+ SPRING_DATASOURCE_USERNAME: postgres
+ SPRING_DATASOURCE_PASSWORD: root
+ SPRING_REDIS_HOST: redis
+ SPRING_REDIS_PORT: 6379
+ depends_on:
+ - postgres
+ - redis
+ redis:
+ image: redis:7.4.2
+ restart: "no"
+ ports:
+ - "6379:6379"
+ volumes:
+ - redis_data:/data
+ healthcheck:
+ test: [ "CMD", "redis-cli", "ping" ]
+ interval: 10s
+ timeout: 5s
+ start_period: 10s
+ retries: 5
+volumes:
+ postgres_data:
+ redis_data:
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 03bc446d..f5a69767 100644
--- a/pom.xml
+++ b/pom.xml
@@ -21,7 +21,24 @@
org.springframework.boot
- spring-boot-starter
+ spring-boot-starter-validation
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+ org.springdoc
+ springdoc-openapi-starter-webmvc-ui
+ 2.6.0
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
@@ -29,6 +46,35 @@
spring-boot-starter-test
test
+
+
+
+ org.springframework.boot
+ spring-boot-starter-jdbc
+ 3.4.2
+
+
+
+
+ org.liquibase
+ liquibase-core
+ 4.31.0
+
+
+
+ org.postgresql
+ postgresql
+ runtime
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-redis
+ 3.4.2
+
+
+
@@ -36,6 +82,14 @@
org.springframework.boot
spring-boot-maven-plugin
+
+
+
+ org.projectlombok
+ lombok
+
+
+
diff --git a/src/main/java/com/emobile/springtodo/config/OpenApiConfig.java b/src/main/java/com/emobile/springtodo/config/OpenApiConfig.java
new file mode 100644
index 00000000..ce0805da
--- /dev/null
+++ b/src/main/java/com/emobile/springtodo/config/OpenApiConfig.java
@@ -0,0 +1,16 @@
+package com.emobile.springtodo.config;
+
+import io.swagger.v3.oas.annotations.OpenAPIDefinition;
+import io.swagger.v3.oas.annotations.info.Contact;
+import io.swagger.v3.oas.annotations.info.Info;
+
+@OpenAPIDefinition(info = @Info(
+ title = "TO-DO List",
+ description = "TO-DO List System",
+ version = "1.0.0",
+ contact = @Contact(
+ name = "Sirik Vadim"
+ )
+))
+class OpenApiConfig {
+}
diff --git a/src/main/java/com/emobile/springtodo/controllers/HandlerExceptionController.java b/src/main/java/com/emobile/springtodo/controllers/HandlerExceptionController.java
new file mode 100644
index 00000000..8b50eebf
--- /dev/null
+++ b/src/main/java/com/emobile/springtodo/controllers/HandlerExceptionController.java
@@ -0,0 +1,222 @@
+package com.emobile.springtodo.controllers;
+
+
+import com.emobile.springtodo.exceptions.*;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.validation.ConstraintViolation;
+import jakarta.validation.ConstraintViolationException;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.dao.DataIntegrityViolationException;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.http.converter.HttpMessageNotReadableException;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+
+import java.time.LocalDateTime;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static com.emobile.springtodo.exceptions.DescriptionUserExeption.GENERATION_ERROR;
+import static com.emobile.springtodo.other.ConstantsClass.LOGGER_SYNTAX;
+
+/**
+ *
+ * Контроллер, обрабатывающий все эксепшены, котоыре могут быть выброшены в процессе работы приложения
+ *
+ */
+@ControllerAdvice
+@Slf4j
+public class HandlerExceptionController {
+
+ /**
+ * Обработчик ExecutorNotFoundException
+ */
+ @ExceptionHandler(EntityNotFoundException.class)
+ protected ResponseEntity handleExecutorNotFoundExeption(EntityNotFoundException e, HttpServletRequest request) {
+ log.error(LOGGER_SYNTAX, GENERATION_ERROR.getEnumDescription(), e.getClass(), e.getMessage(), e);
+
+ ApiErrorResponse apiErrorResponse = buildApiErrorResponse(
+ HttpStatus.NOT_FOUND,
+ e.getMessage(),
+ request.getRequestURI(),
+ null
+ );
+
+ return new ResponseEntity<>(apiErrorResponse, HttpStatus.NOT_FOUND);
+ }
+
+ @ExceptionHandler(DataCalendarNotBeNullException.class)
+ protected ResponseEntity handleExecutorNotFoundExeption(DataCalendarNotBeNullException e, HttpServletRequest request) {
+ log.error(LOGGER_SYNTAX, GENERATION_ERROR.getEnumDescription(), e.getClass(), e.getMessage(), e);
+
+ ApiErrorResponse apiErrorResponse = buildApiErrorResponse(
+ HttpStatus.NOT_ACCEPTABLE,
+ e.getMessage(),
+ request.getRequestURI(),
+ null
+ );
+
+ return new ResponseEntity<>(apiErrorResponse, HttpStatus.NOT_ACCEPTABLE);
+ }
+
+ @ExceptionHandler(StatusTaskNotBeNullException.class)
+ protected ResponseEntity handleExecutorNotFoundExeption(StatusTaskNotBeNullException e, HttpServletRequest request) {
+ log.error(LOGGER_SYNTAX, GENERATION_ERROR.getEnumDescription(), e.getClass(), e.getMessage(), e);
+
+ ApiErrorResponse apiErrorResponse = buildApiErrorResponse(
+ HttpStatus.NOT_ACCEPTABLE,
+ e.getMessage(),
+ request.getRequestURI(),
+ null
+ );
+
+ return new ResponseEntity<>(apiErrorResponse, HttpStatus.NOT_ACCEPTABLE);
+ }
+
+ /**
+ * Обработчик ConstraintViolationException
+ */
+ @ExceptionHandler(ConstraintViolationException.class)
+ protected ResponseEntity handleConstraintViolationException(ConstraintViolationException e,
+ HttpServletRequest request) {
+ log.error(LOGGER_SYNTAX, GENERATION_ERROR.getEnumDescription(), e.getClass(), e.getMessage(), e);
+
+ Set> violationsSet = e.getConstraintViolations();
+ List violations = (violationsSet != null)
+ ? violationsSet.stream()
+ .map(violation -> new Violation(
+ violation.getPropertyPath().toString(),
+ violation.getMessage()
+ ))
+ .collect(Collectors.toList())
+ : Collections.emptyList();
+
+ ApiErrorResponse apiErrorResponse = buildApiErrorResponse(
+ HttpStatus.BAD_REQUEST,
+ e.getMessage(),
+ request.getRequestURI(),
+ violations
+ );
+
+ return new ResponseEntity<>(apiErrorResponse, HttpStatus.BAD_REQUEST);
+ }
+
+ /**
+ * Обработчик MethodArgumentNotValidException
+ */
+ @ExceptionHandler(MethodArgumentNotValidException.class)
+ protected ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException e, HttpServletRequest request) {
+ log.error(LOGGER_SYNTAX, GENERATION_ERROR.getEnumDescription(), e.getClass(), e.getMessage(), e);
+
+ List violations = e.getBindingResult().getFieldErrors().stream()
+ .map(error -> new Violation(error.getField(), error.getDefaultMessage()))
+ .collect(Collectors.toList());
+
+ ApiErrorResponse apiErrorResponse = buildApiErrorResponse(
+ HttpStatus.BAD_REQUEST,
+ e.getMessage(),
+ request.getRequestURI(),
+ violations
+ );
+
+ return new ResponseEntity<>(apiErrorResponse, HttpStatus.BAD_REQUEST);
+ }
+
+ /**
+ * Обработчик HttpMessageNotReadableException
+ */
+ @ExceptionHandler(HttpMessageNotReadableException.class)
+ protected ResponseEntity handleHttpMessageNotReadableException(HttpMessageNotReadableException e, HttpServletRequest request) {
+ log.error(LOGGER_SYNTAX, GENERATION_ERROR.getEnumDescription(), e.getClass(), e.getMessage(), e);
+
+ ApiErrorResponse apiErrorResponse = buildApiErrorResponse(
+ HttpStatus.BAD_REQUEST,
+ e.getMessage(),
+ request.getRequestURI(),
+ null
+ );
+
+ return new ResponseEntity<>(apiErrorResponse, HttpStatus.BAD_REQUEST);
+ }
+
+ /**
+ * Обработчик IllegalStateException
+ */
+ @ExceptionHandler(IllegalStateException.class)
+ protected ResponseEntity handleIllegalStateException(IllegalStateException e, HttpServletRequest request) {
+ log.error(LOGGER_SYNTAX, GENERATION_ERROR.getEnumDescription(), e.getClass(), e.getMessage(), e);
+
+ ApiErrorResponse apiErrorResponse = buildApiErrorResponse(
+ HttpStatus.INTERNAL_SERVER_ERROR,
+ e.getMessage(),
+ request.getRequestURI(),
+ null
+ );
+
+ return new ResponseEntity<>(apiErrorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+
+ /**
+ * Обработчик DataIntegrityViolationException
+ */
+ @ExceptionHandler(DataIntegrityViolationException.class)
+ protected ResponseEntity handleDataIntegrityViolationException(DataIntegrityViolationException e,
+ HttpServletRequest request) {
+ log.error(LOGGER_SYNTAX, GENERATION_ERROR.getEnumDescription(), e.getClass(), e.getMessage(), e);
+
+ ApiErrorResponse apiErrorResponse = buildApiErrorResponse(
+ HttpStatus.INTERNAL_SERVER_ERROR,
+ e.getMessage(),
+ request.getRequestURI(),
+ null
+ );
+
+ return new ResponseEntity<>(apiErrorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+
+ /**
+ * Общий обработчик для всех остальных исключений
+ */
+ @ExceptionHandler(Exception.class)
+ protected ResponseEntity handleAllExceptions(Exception e, HttpServletRequest request) {
+ log.error(LOGGER_SYNTAX, GENERATION_ERROR.getEnumDescription(), e.getClass(), e.getMessage(), e);
+
+ ApiErrorResponse apiErrorResponse = buildApiErrorResponse(
+ HttpStatus.INTERNAL_SERVER_ERROR,
+ e.getMessage(),
+ request.getRequestURI(),
+ null
+ );
+
+ return new ResponseEntity<>(apiErrorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+
+ @ExceptionHandler(RuntimeException.class)
+ protected ResponseEntity handleAllRunTimeException(RuntimeException e, HttpServletRequest request) {
+ log.error(LOGGER_SYNTAX, GENERATION_ERROR.getEnumDescription(), e.getClass(), e.getMessage(), e);
+
+ ApiErrorResponse apiErrorResponse = buildApiErrorResponse(
+ HttpStatus.INTERNAL_SERVER_ERROR,
+ e.getMessage(),
+ request.getRequestURI(),
+ null
+ );
+
+ return new ResponseEntity<>(apiErrorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+
+ private ApiErrorResponse buildApiErrorResponse(HttpStatus status, String message, String path, List violations) {
+ return ApiErrorResponse.builder()
+ .timestamp(LocalDateTime.now())
+ .status(status.value())
+ .error(status.getReasonPhrase())
+ .message(message)
+ .path(path)
+ .violations(violations)
+ .build();
+ }
+}
diff --git a/src/main/java/com/emobile/springtodo/controllers/TaskController.java b/src/main/java/com/emobile/springtodo/controllers/TaskController.java
new file mode 100644
index 00000000..b712e8d4
--- /dev/null
+++ b/src/main/java/com/emobile/springtodo/controllers/TaskController.java
@@ -0,0 +1,142 @@
+package com.emobile.springtodo.controllers;
+
+import com.emobile.springtodo.dto.TaskDto;
+import com.emobile.springtodo.exceptions.DataCalendarNotBeNullException;
+import com.emobile.springtodo.exceptions.EntityNotFoundException;
+import com.emobile.springtodo.exceptions.StatusTaskNotBeNullException;
+import com.emobile.springtodo.services.TaskService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.ExampleObject;
+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.Min;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Optional;
+
+import static com.emobile.springtodo.other.ConstantsClass.IS_DELETE;
+
+/**
+ *
+ * Контроллер создания, редактирования, удаления, получения информации о задачах
+ *
+ */
+@Tag(name = "Задачи", description = "Позволяет создать, редактировать, удалить, получить информацию о задачах")
+@RestController
+@RequestMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
+@Slf4j
+@Validated
+public class TaskController {
+
+ private final TaskService taskService;
+
+ @Autowired
+ TaskController(TaskService taskService) {
+ this.taskService = taskService;
+ }
+
+ /**
+ * Эндпоинд POST для создания задачи
+ *
+ * @param tasksDto DTO объект задачи для создания
+ * @return {@link ResponseEntity} с созданной задачей
+ */
+ @Operation(summary = "Создание задачи", description = "Позволяет создать задачу")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Задача успешно создана", content = @Content(examples = @ExampleObject(value = "\"{\\n \\\"id\\\": 1,\\n \\\"title\\\": \\\"Тестовая задача\\\",\\n \\\"description\\\": \\\"Тестовое описание задачи\\\",\\n \\\"status\\\": true,\\n \\\"dateCalendar\\\": \\\"2020-01-01T00:01:10\\\"\\n}\""))),
+ @ApiResponse(responseCode = "400", description = "Не удалось создать задачу", content = @Content)
+ })
+ @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Пример тела запроса для создания задачи", content = @Content(mediaType = "application/json", examples = @ExampleObject(name = "Создание задачи", value = "\"{\\n \\\"id\\\": 1,\\n \\\"title\\\": \\\"Тестовая задача\\\",\\n \\\"description\\\": \\\"Тестовое описание задачи\\\",\\n \\\"status\\\": true,\\n \\\"dateCalendar\\\": \\\"2020-01-01T00:01:10\\\"\\n}\"")))
+ @PostMapping(value = "/task/create")
+ public ResponseEntity createTask(@Valid @RequestBody @NotNull(message = "Объект не может быть null")
+ TaskDto tasksDto) throws DataCalendarNotBeNullException, StatusTaskNotBeNullException {
+ log.info("Создание задачи, POST ");
+ TaskDto localTasksDto = taskService.createTasks(tasksDto);
+ if (localTasksDto != null) {
+ return ResponseEntity.ok(localTasksDto);
+ }
+ return ResponseEntity.badRequest().build();
+ }
+
+ /**
+ * Эндпоинд GET для получения информации о задачах по исполнителю
+ *
+ * @param taskTitle - емейл пользователя, по которому будут найдены задачи, комментарии
+ * @param offset - номер страницы
+ * @param limit - кол-во сущностей на странице
+ * @return {@link ResponseEntity>}
+ */
+ @Operation(summary = "Получить задачи по заголовку")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Возвращены задачи по заголовку", content = @Content(examples = @ExampleObject(value = "\"{\\n \\\"id\\\": 1,\\n \\\"title\\\": \\\"Тестовая задача\\\",\\n \\\"description\\\": \\\"Тестовое описание задачи\\\",\\n \\\"status\\\": true,\\n \\\"dateCalendar\\\": \\\"2020-01-01T00:01:10\\\"\\n}\""))),
+ @ApiResponse(responseCode = "400", description = "Не удалось получить задачи по заголовку", content = @Content)
+ })
+ @GetMapping(value = "/task/list/{title}", consumes = MediaType.ALL_VALUE)
+ public ResponseEntity> getTasksByTitle(
+ @PathVariable("title") @Parameter(description = "Заголовок задачи") @NotBlank(message = "Заголовок задачи не может быть пустым")
+ String taskTitle,
+ @RequestParam(value = "offset", defaultValue = "0") @Min(0) @Parameter(description = "Номер страницы", example = "0") @NotNull(message = "Страница не может быть пустой")
+ Integer offset,
+ @RequestParam(value = "limit", defaultValue = "10") @Min(10) @Parameter(description = "Количество сущностей на странице",
+ example = "10") @NotNull(message = "Кол-во сущностей не может быть пустым") Integer limit
+ ) {
+ log.info("Получение задачи по исполнителю, метод GET " + taskTitle);
+ Optional> optionalExecutorTasksDtoList = taskService
+ .getTasksByTitle(taskTitle, offset, limit);
+ return optionalExecutorTasksDtoList.map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.badRequest().build());
+ }
+
+ /**
+ * Эндпоинд PUT для редактирования задач
+ *
+ * @param tasksDto объект {@link TaskDto}
+ * @return {@link ResponseEntity<>}
+ */
+ @Operation(summary = "Отредактировать задачу")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Задача успешно отредактирована", content = @Content(examples = @ExampleObject(value = "\"{\\n \\\"id\\\": 1,\\n \\\"title\\\": \\\"Тестовая задача\\\",\\n \\\"description\\\": \\\"Тестовое описание задачи\\\",\\n \\\"status\\\": true,\\n \\\"dateCalendar\\\": \\\"2020-01-01T00:01:10\\\"\\n}\""))),
+ @ApiResponse(responseCode = "400", description = "Не удалось отредактировать задачу", content = @Content)
+ })
+ @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Пример тела запроса для редактирования задачи", content = @Content(mediaType = "application/json", examples = @ExampleObject(name = "Редактирование задачи", value = "\"{\\n \\\"id\\\": 1,\\n \\\"title\\\": \\\"Тестовая задача\\\",\\n \\\"description\\\": \\\"Тестовое описание задачи\\\",\\n \\\"status\\\": true,\\n \\\"dateCalendar\\\": \\\"2020-01-01T00:01:10\\\"\\n}\"")))
+ @PutMapping("/task/update-tasks")
+ public ResponseEntity editTasks(@Valid @RequestBody @Parameter(description = "Объект TasksDto с полями, требующими редактирования")
+ @NotNull(message = "TasksDto не может быть null") TaskDto tasksDto) throws EntityNotFoundException {
+ Optional newTasksDto = taskService
+ .changeTasks(tasksDto);
+ return newTasksDto.map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.badRequest().build());
+ }
+
+ /**
+ * Эндпоинд DELETE для удаления задач
+ *
+ * @param idTasks - id задачи для удаления
+ * @return {@link ResponseEntity}
+ */
+ @Operation(summary = "Удалить задачу")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Задача успешно отредактирована", content = @Content(examples = @ExampleObject(value = "{ \"message\": \"Сущность была удалена\" }"))),
+ @ApiResponse(responseCode = "400", description = "Не удалось произвести удаление задачи", content = @Content)
+ })
+ @DeleteMapping(value = "/task/delete/{id}", consumes = MediaType.ALL_VALUE)
+ public ResponseEntity deleteTasks(@PathVariable("id") @Parameter(description = "ID задачи") @NotNull(message = "ID задачи не может быть null")
+ Long idTasks) {
+ log.info("Удаление задачи по id, метод DELETE" + idTasks);
+ boolean result = taskService.deleteTasks(idTasks);
+ if (result) {
+ return ResponseEntity.ok(IS_DELETE);
+ }
+ return ResponseEntity.noContent().build();
+ }
+}
diff --git a/src/main/java/com/emobile/springtodo/dto/TaskDto.java b/src/main/java/com/emobile/springtodo/dto/TaskDto.java
new file mode 100644
index 00000000..14468b9f
--- /dev/null
+++ b/src/main/java/com/emobile/springtodo/dto/TaskDto.java
@@ -0,0 +1,47 @@
+package com.emobile.springtodo.dto;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.Max;
+import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import org.springframework.validation.annotation.Validated;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+@Getter
+@Setter
+@AllArgsConstructor
+@NoArgsConstructor
+@Validated
+public class TaskDto implements Serializable {
+
+ @Schema(description = "Уникальный идентификатор задачи", example = "1")
+ @Min(value = 1, message = "Идентификатор задачи не должен быть меньше единицы")
+ @Max(value = 2147483647, message = "Идентификатор задачи не должен быть больше 2147483647")
+ private Long id;
+
+ @Schema(description = "Заголовок задачи", example = "Тестовая задача")
+ @NotBlank(message = "Заголовок не может быть пуст")
+ @Size(min = 1, message = "Длина заголовка задачи не должна быть меньше 1 символа")
+ @Size(max = 2000, message = "Длина заголовка задачи не должна быть больше 2 тысяч символов")
+ private String title;
+
+ @Schema(description = "Описание задачи", example = "Тестовое описание задачи")
+ @Size(max = 30000, message = "Длина описания не должна превышать 30000 символов")
+ private String description;
+
+ @Schema(description = "Статус задачи", example = "true")
+ private Boolean status;
+
+ @Schema(description = "Дата задачи в календаре", example = "2020-01-01T00:01:10")
+ @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss")
+ private LocalDateTime dateCalendar;
+
+}
diff --git a/src/main/java/com/emobile/springtodo/entities/Task.java b/src/main/java/com/emobile/springtodo/entities/Task.java
new file mode 100644
index 00000000..65917748
--- /dev/null
+++ b/src/main/java/com/emobile/springtodo/entities/Task.java
@@ -0,0 +1,26 @@
+package com.emobile.springtodo.entities;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.time.LocalDateTime;
+
+@Getter
+@Setter
+@AllArgsConstructor
+@NoArgsConstructor
+public class Task {
+
+ private Long id;
+
+ private String title;
+
+ private String description;
+
+ private Boolean status;
+
+ private LocalDateTime dateCalendar;
+
+}
diff --git a/src/main/java/com/emobile/springtodo/exceptions/ApiErrorResponse.java b/src/main/java/com/emobile/springtodo/exceptions/ApiErrorResponse.java
new file mode 100644
index 00000000..50fbc4ea
--- /dev/null
+++ b/src/main/java/com/emobile/springtodo/exceptions/ApiErrorResponse.java
@@ -0,0 +1,24 @@
+package com.emobile.springtodo.exceptions;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import lombok.*;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class ApiErrorResponse {
+
+ private LocalDateTime timestamp;
+ private int status;
+ private String error;
+ private String message;
+ private String path;
+ private List violations;
+
+}
diff --git a/src/main/java/com/emobile/springtodo/exceptions/DataCalendarNotBeNullException.java b/src/main/java/com/emobile/springtodo/exceptions/DataCalendarNotBeNullException.java
new file mode 100644
index 00000000..7e4cd8de
--- /dev/null
+++ b/src/main/java/com/emobile/springtodo/exceptions/DataCalendarNotBeNullException.java
@@ -0,0 +1,10 @@
+package com.emobile.springtodo.exceptions;
+
+/**
+ * Эксепшен, который выбрасывается, если поле дата задачи у сущности Задача null
+ */
+public class DataCalendarNotBeNullException extends Exception {
+ public DataCalendarNotBeNullException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/com/emobile/springtodo/exceptions/DescriptionUserExeption.java b/src/main/java/com/emobile/springtodo/exceptions/DescriptionUserExeption.java
new file mode 100644
index 00000000..696a9ffc
--- /dev/null
+++ b/src/main/java/com/emobile/springtodo/exceptions/DescriptionUserExeption.java
@@ -0,0 +1,27 @@
+package com.emobile.springtodo.exceptions;
+
+/**
+ *
+ * Enum с описанием для кастомных ошибок, которые могут выбрасываться в некоторых частях приложения
+ *
+ */
+public enum DescriptionUserExeption {
+
+ GENERATION_ERROR("Возникла ошибка в системе: "),
+
+ TASK_NOT_FOUND_BY_ID("Задача по переданному ID не найдена"),
+
+ STATUS_NOT_BE_NULL("Статус задачи не может быть пуст"),
+
+ DATE_CALENDAR_NOT_BE_NULL("Дата задачи в календаре не может быть пустой");
+
+ private String enumDescription;
+
+ DescriptionUserExeption(String enumDescription) {
+ this.enumDescription = enumDescription;
+ }
+
+ public String getEnumDescription() {
+ return enumDescription;
+ }
+}
diff --git a/src/main/java/com/emobile/springtodo/exceptions/EntityNotFoundException.java b/src/main/java/com/emobile/springtodo/exceptions/EntityNotFoundException.java
new file mode 100644
index 00000000..735672db
--- /dev/null
+++ b/src/main/java/com/emobile/springtodo/exceptions/EntityNotFoundException.java
@@ -0,0 +1,10 @@
+package com.emobile.springtodo.exceptions;
+
+/**
+ * Эксепшен, который выбрасывается, если сущность не найдена
+ */
+public class EntityNotFoundException extends Exception {
+ public EntityNotFoundException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/com/emobile/springtodo/exceptions/StatusTaskNotBeNullException.java b/src/main/java/com/emobile/springtodo/exceptions/StatusTaskNotBeNullException.java
new file mode 100644
index 00000000..b3734b46
--- /dev/null
+++ b/src/main/java/com/emobile/springtodo/exceptions/StatusTaskNotBeNullException.java
@@ -0,0 +1,10 @@
+package com.emobile.springtodo.exceptions;
+
+/**
+ * Эксепшен, который выбрасывается, если поле Статус у сущности Задача null
+ */
+public class StatusTaskNotBeNullException extends Exception {
+ public StatusTaskNotBeNullException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/com/emobile/springtodo/exceptions/Violation.java b/src/main/java/com/emobile/springtodo/exceptions/Violation.java
new file mode 100644
index 00000000..1d0eba84
--- /dev/null
+++ b/src/main/java/com/emobile/springtodo/exceptions/Violation.java
@@ -0,0 +1,3 @@
+package com.emobile.springtodo.exceptions;
+
+public record Violation(String fieldName, String message) {}
diff --git a/src/main/java/com/emobile/springtodo/mappers/TaskMapper.java b/src/main/java/com/emobile/springtodo/mappers/TaskMapper.java
new file mode 100644
index 00000000..7aa7c367
--- /dev/null
+++ b/src/main/java/com/emobile/springtodo/mappers/TaskMapper.java
@@ -0,0 +1,42 @@
+package com.emobile.springtodo.mappers;
+
+import com.emobile.springtodo.dto.TaskDto;
+import com.emobile.springtodo.entities.Task;
+
+import java.util.List;
+
+/**
+ *
+ * Маппер для {@link }, {@link }
+ *
+ */
+public interface TaskMapper {
+
+ /**
+ * Метод, конвертирующий {@link TaskDto} в {@link Task}
+ *
+ * @param tasksDto DTO объекта задачи
+ * @return Сконвертированный объект задачи
+ */
+ Task convertDtoToTasks(TaskDto tasksDto);
+
+ /**
+ * Метод, конвертирующий {@link Task} в {@link TaskDto}
+ *
+ * @param tasks Сущность задачи из базы данных
+ * @return Сконвертированный DTO объекта задачи
+ */
+ TaskDto convertTasksToDto(Task tasks);
+
+ /**
+ * Метод, сравнивающий поля Tasks из БД с TasksDto
+ *
+ * @param tasksDto DTO объекта задачи
+ * @param tasks Сущность задачи из базы данных
+ * @return Обновленный объект задачи {@link }
+ */
+ void compareTaskAndDto(TaskDto tasksDto, Task tasks);
+
+ List transferListTasksToDto(List tasksList);
+
+}
diff --git a/src/main/java/com/emobile/springtodo/mappers/TaskMapperImpl.java b/src/main/java/com/emobile/springtodo/mappers/TaskMapperImpl.java
new file mode 100644
index 00000000..fd0d5777
--- /dev/null
+++ b/src/main/java/com/emobile/springtodo/mappers/TaskMapperImpl.java
@@ -0,0 +1,88 @@
+package com.emobile.springtodo.mappers;
+
+import com.emobile.springtodo.dto.TaskDto;
+import com.emobile.springtodo.entities.Task;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Component
+@Slf4j
+public class TaskMapperImpl implements TaskMapper {
+
+ @Override
+ public Task convertDtoToTasks(TaskDto tasksDto) {
+ log.info("Метод convertDtoToTasks()");
+ Task taskLocalObject = new Task();
+ if (tasksDto != null) {
+ taskLocalObject.setId(tasksDto.getId());
+ taskLocalObject.setTitle(tasksDto.getTitle());
+ taskLocalObject.setDescription(tasksDto.getDescription());
+ taskLocalObject.setStatus(tasksDto.getStatus());
+ taskLocalObject.setDateCalendar(tasksDto.getDateCalendar());
+ }
+ return taskLocalObject;
+ }
+
+ @Override
+ public TaskDto convertTasksToDto(Task tasks) {
+ log.info("Метод convertTasksToDto()");
+ TaskDto tasksDtoLocalObject = new TaskDto();
+ if (tasks != null) {
+ tasksDtoLocalObject.setId(tasks.getId());
+ tasksDtoLocalObject.setTitle(tasks.getTitle());
+ tasksDtoLocalObject.setDescription(tasks.getDescription());
+ tasksDtoLocalObject.setStatus(tasks.getStatus());
+ tasksDtoLocalObject.setDateCalendar(tasks.getDateCalendar());
+ }
+ return tasksDtoLocalObject;
+ }
+
+ @Override
+ public List transferListTasksToDto(List tasksList) {
+ List taskDtoList = new ArrayList<>();
+ if (tasksList != null) {
+ for (Task tasks : tasksList) {
+ TaskDto taskDto = convertTasksToDto(tasks);
+ taskDtoList.add(taskDto);
+ }
+ }
+ return taskDtoList;
+ }
+
+ @Override
+ public void compareTaskAndDto(TaskDto tasksDto, Task tasks) {
+ if (tasksDto != null) {
+ compareTasksAndDtoTitle(tasksDto, tasks);
+ compareTasksAndDtoDescription(tasksDto, tasks);
+ compareTasksAndDtoStatus(tasksDto, tasks);
+ compareTasksAndDtoDateCalendar(tasksDto, tasks);
+ }
+ }
+
+ private void compareTasksAndDtoTitle(TaskDto tasksDto, Task tasks) {
+ if (tasksDto.getTitle() != null) {
+ tasks.setTitle(tasksDto.getTitle());
+ }
+ }
+
+ private void compareTasksAndDtoDescription(TaskDto tasksDto, Task tasks) {
+ if (tasksDto.getDescription() != null) {
+ tasks.setDescription(tasksDto.getDescription());
+ }
+ }
+
+ private void compareTasksAndDtoStatus(TaskDto tasksDto, Task tasks) {
+ if (tasksDto.getStatus() != null) {
+ tasks.setStatus(tasksDto.getStatus());
+ }
+ }
+
+ private void compareTasksAndDtoDateCalendar(TaskDto tasksDto, Task tasks) {
+ if (tasksDto.getDateCalendar() != null) {
+ tasks.setDateCalendar(tasksDto.getDateCalendar());
+ }
+ }
+}
diff --git a/src/main/java/com/emobile/springtodo/other/ConstantsClass.java b/src/main/java/com/emobile/springtodo/other/ConstantsClass.java
new file mode 100644
index 00000000..c77d18e4
--- /dev/null
+++ b/src/main/java/com/emobile/springtodo/other/ConstantsClass.java
@@ -0,0 +1,23 @@
+package com.emobile.springtodo.other;
+
+/**
+ *
+ * Класс с константами
+ *
+ */
+public class ConstantsClass {
+
+ public static final char PERCENT_CHAR_PRIMITIVE = '%';
+ public static final Integer ZERO = 0;
+ public static final Integer ONE = 1;
+ public static final String IS_DELETE = "Entity was deleted";
+ public static final String ID_FIELD = "id";
+ public static final String TITLE_FIELD = "title";
+ public static final String DESCRIPTION_FIELD = "description";
+ public static final String STATUS_FIELD = "status";
+ public static final String DATA_CALENDAR_FIELD = "date_calendar";
+ public static final String LINE_FEED = "\n";
+ public static final String PERCENT = "%";
+ public static final String LOGGER_SYNTAX = "{}{}\\n{}\\n{}";
+
+}
diff --git a/src/main/java/com/emobile/springtodo/repositories/JdbcRepository.java b/src/main/java/com/emobile/springtodo/repositories/JdbcRepository.java
new file mode 100644
index 00000000..f56db361
--- /dev/null
+++ b/src/main/java/com/emobile/springtodo/repositories/JdbcRepository.java
@@ -0,0 +1,50 @@
+package com.emobile.springtodo.repositories;
+
+/**
+ * Интерфейс-репозиторий с общими методами для запросов к БД
+ *
+ * @param Параметр, в котором идёт инкремент id у сущности
+ * @param Класс, который будет сохраняться в БД
+ */
+public interface JdbcRepository {
+
+ /**
+ * Метод, позволяющий проверить в БД, существует ли сущность по переданному ID
+ *
+ * @param id id сущности
+ * @return true, если сущность есть в БД
+ */
+ boolean existsById(T id);
+
+ /**
+ * Метод, позволяющий сохранить объект в БД
+ *
+ * @param entity объект, который нужно сохранить в БД
+ * @return сохраненный в БД объект
+ */
+ R save(R entity);
+
+ /**
+ * Метод, позволяющий обновить объект в БД
+ *
+ * @param newEntity объект, который нужно обновить в БД
+ * @return обновленный в БД объект
+ */
+ R update(R newEntity);
+
+ /**
+ * Метод, позволяющий найти объект в БД по переданному ID
+ *
+ * @param id сущности
+ * @return объект из БД, найденный по ID
+ */
+ R findById(T id);
+
+ /**
+ * Метод, позволяющий удалить сущность из БД
+ *
+ * @param id сущности
+ */
+ void deleteById(T id);
+
+}
diff --git a/src/main/java/com/emobile/springtodo/repositories/TaskRepository.java b/src/main/java/com/emobile/springtodo/repositories/TaskRepository.java
new file mode 100644
index 00000000..9ad3c138
--- /dev/null
+++ b/src/main/java/com/emobile/springtodo/repositories/TaskRepository.java
@@ -0,0 +1,15 @@
+package com.emobile.springtodo.repositories;
+
+import com.emobile.springtodo.entities.Task;
+
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Интерфейс-репозиторий с кастомными методами для task
+ */
+public interface TaskRepository extends JdbcRepository {
+
+ Optional> findAllByTitle(String title, Integer offset, Integer limit);
+
+}
diff --git a/src/main/java/com/emobile/springtodo/repositories/TaskRepositoryImpl.java b/src/main/java/com/emobile/springtodo/repositories/TaskRepositoryImpl.java
new file mode 100644
index 00000000..a720e155
--- /dev/null
+++ b/src/main/java/com/emobile/springtodo/repositories/TaskRepositoryImpl.java
@@ -0,0 +1,139 @@
+package com.emobile.springtodo.repositories;
+
+import com.emobile.springtodo.entities.Task;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.core.PreparedStatementCreator;
+import org.springframework.jdbc.core.RowMapper;
+import org.springframework.jdbc.support.GeneratedKeyHolder;
+import org.springframework.jdbc.support.KeyHolder;
+import org.springframework.stereotype.Repository;
+
+import java.sql.PreparedStatement;
+import java.sql.Timestamp;
+import java.util.List;
+import java.util.Optional;
+
+import static com.emobile.springtodo.other.ConstantsClass.*;
+
+@Slf4j
+@Repository
+public class TaskRepositoryImpl implements TaskRepository {
+
+ private final JdbcTemplate jdbcTemplate;
+
+ @Autowired
+ public TaskRepositoryImpl(JdbcTemplate jdbcTemplate) {
+ this.jdbcTemplate = jdbcTemplate;
+ }
+
+ @Override
+ public boolean existsById(Long id) {
+ String sql = "SELECT COUNT(*) FROM tasks_entity WHERE id = ?";
+ Integer count = jdbcTemplate.queryForObject(sql, Integer.class, id);
+ if (count != null) {
+ return count > 0;
+ }
+ return false;
+ }
+
+ @Override
+ public Task save(Task entity) {
+ String sql = "INSERT INTO tasks_entity (title, description, status, date_calendar) " +
+ "VALUES (?, ?, ?, ?)";
+ commonUpdate(sql, entity);
+ return entity;
+ }
+
+ @Override
+ public Task update(Task newEntity) {
+ String sql = "UPDATE tasks_entity SET title = ?, description = ?, status = ?, date_calendar = ? WHERE id = ? ";
+ commonUpdate(sql, newEntity);
+ return newEntity;
+ }
+
+ @Override
+ public Task findById(Long id) {
+ String sql = "SELECT id, title, description, status, date_calendar " +
+ "FROM tasks_entity WHERE id = ?";
+ return jdbcTemplate.queryForObject(sql, rowMapper(), id);
+ }
+
+ @Override
+ public Optional> findAllByTitle(String title, Integer offset, Integer limit) {
+ String sql = "SELECT id, title, description, status, date_calendar " +
+ "FROM tasks_entity WHERE title LIKE ? ORDER BY id LIMIT ? OFFSET ?";
+ return Optional.of(jdbcTemplate.query(sql, rowMapper(), title, limit, offset));
+ }
+
+ @Override
+ public void deleteById(Long id) {
+ String sql = "DELETE FROM tasks_entity WHERE id = ?";
+ jdbcTemplate.update(sql, id);
+ log.info("DEL method");
+ }
+
+ private RowMapper rowMapper() {
+ return (rs, rowNum) -> {
+ Task task = new Task();
+ task.setId(rs.getLong(ID_FIELD));
+ task.setTitle(rs.getString(TITLE_FIELD));
+ task.setDescription(rs.getString(DESCRIPTION_FIELD));
+ task.setStatus(rs.getBoolean(STATUS_FIELD));
+ task.setDateCalendar(rs.getTimestamp(DATA_CALENDAR_FIELD).toLocalDateTime()); // ✅ Timestamp → LocalDateTime
+ return task;
+ };
+ }
+
+ /**
+ * Метод, сохраняющий, либо обновляющий запись в таблице. В объекте поле id null, если сохранение, либо обновление не удалось
+ * (т.к update возвращает кол-во измененных строк)
+ *
+ * @param sql SQL - запрос на обновление сущности, либо на сохранение
+ * @param entity Объект для сохрнения, либо обновления в БД
+ */
+ private void commonUpdate(String sql, Task entity) {
+ KeyHolder keyHolder = new GeneratedKeyHolder();
+ jdbcTemplate.update(preparedStatement(sql, entity), keyHolder);
+ Number key = keyHolder.getKey();
+ if (key != null) {
+ entity.setId(key.longValue());
+ }
+ }
+
+ /**
+ * Метод, собирающий PreparedStatement через интерфейс PreparedStatementCreator для отправки запроса с параметрами к БД
+ *
+ * @param sql SQL - запрос на обновление сущности, либо на сохранение
+ * @param entity Объект для сохрнения, либо обновления в БД
+ * @return интерфейс {@link PreparedStatementCreator}
+ */
+ private PreparedStatementCreator preparedStatement(String sql, Task entity) {
+
+ return connection -> {
+ PreparedStatement ps = connection.prepareStatement(sql, new String[]{ID_FIELD});
+ int counter = 1;
+ if (entity.getTitle() != null) {
+ ps.setString(counter, entity.getTitle());
+ counter++;
+ }
+ if (entity.getDescription() != null) {
+ ps.setString(counter, entity.getDescription());
+ counter++;
+ }
+ if (entity.getStatus() != null) {
+ ps.setBoolean(counter, entity.getStatus());
+ counter++;
+ }
+ if (entity.getDateCalendar() != null) {
+ ps.setTimestamp(counter, Timestamp.valueOf(entity.getDateCalendar()));
+ counter++;
+ }
+ if (entity.getId() != null) {
+ ps.setLong(counter, entity.getId());
+ }
+ return ps;
+ };
+ }
+}
diff --git a/src/main/java/com/emobile/springtodo/services/TaskService.java b/src/main/java/com/emobile/springtodo/services/TaskService.java
new file mode 100644
index 00000000..a59e6787
--- /dev/null
+++ b/src/main/java/com/emobile/springtodo/services/TaskService.java
@@ -0,0 +1,52 @@
+package com.emobile.springtodo.services;
+
+import com.emobile.springtodo.dto.TaskDto;
+import com.emobile.springtodo.exceptions.DataCalendarNotBeNullException;
+import com.emobile.springtodo.exceptions.EntityNotFoundException;
+import com.emobile.springtodo.exceptions.StatusTaskNotBeNullException;
+
+import java.util.List;
+import java.util.Optional;
+
+/**
+ *
+ * Интерфейс для работы с сущностью Tasks
+ *
+ */
+public interface TaskService {
+
+ /**
+ * Метод для создания задачи
+ *
+ * @param tasksDto DTO объекта задачи, содержащий данные для создания
+ * @return Созданный объект-задача в виде {@link TaskDto}
+ */
+ TaskDto createTasks(TaskDto tasksDto) throws StatusTaskNotBeNullException, DataCalendarNotBeNullException;
+
+ /**
+ * Метод для редактирования задачи
+ *
+ * @param tasksDto DTO объекта задачи с обновленными данными
+ * @return {@link Optional} с изменённым объектом-задачей, если задача найдена и успешно обновлена
+ */
+ Optional changeTasks(TaskDto tasksDto) throws EntityNotFoundException;
+
+ /**
+ * Метод для получения задач по заголовку
+ *
+ * @param authorOrExecutor Название задачи
+ * @param offset Номер страницы для пагинации
+ * @param limit Лимит записей на странице
+ * @return {@link Optional} со списком задач в виде {@link List<>}
+ */
+ Optional> getTasksByTitle(String authorOrExecutor, Integer offset, Integer limit);
+
+ /**
+ * Метод для удаления задачи
+ *
+ * @param idTasks ID задачи, которую нужно удалить
+ * @return true - если задача найдена и удалена успешно
+ */
+ boolean deleteTasks(Long idTasks);
+
+}
diff --git a/src/main/java/com/emobile/springtodo/services/TaskServicesImpl.java b/src/main/java/com/emobile/springtodo/services/TaskServicesImpl.java
new file mode 100644
index 00000000..288f31b6
--- /dev/null
+++ b/src/main/java/com/emobile/springtodo/services/TaskServicesImpl.java
@@ -0,0 +1,103 @@
+package com.emobile.springtodo.services;
+
+import com.emobile.springtodo.dto.TaskDto;
+import com.emobile.springtodo.entities.Task;
+import com.emobile.springtodo.exceptions.DataCalendarNotBeNullException;
+import com.emobile.springtodo.exceptions.EntityNotFoundException;
+import com.emobile.springtodo.exceptions.StatusTaskNotBeNullException;
+import com.emobile.springtodo.mappers.TaskMapper;
+import com.emobile.springtodo.repositories.TaskRepository;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.CachePut;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+import java.util.Optional;
+
+import static com.emobile.springtodo.exceptions.DescriptionUserExeption.*;
+import static com.emobile.springtodo.other.ConstantsClass.*;
+
+@Service
+@Slf4j
+public class TaskServicesImpl implements TaskService {
+
+ private final TaskMapper taskMapper;
+ private final TaskRepository taskRepository;
+
+ @Autowired
+ public TaskServicesImpl(TaskMapper taskMapper, TaskRepository taskRepository) {
+ this.taskMapper = taskMapper;
+ this.taskRepository = taskRepository;
+ }
+
+ @CachePut(cacheNames = "taskServiceCache", key = "#result.id")
+ @Transactional
+ @Override
+ public TaskDto createTasks(TaskDto tasksDto) throws StatusTaskNotBeNullException, DataCalendarNotBeNullException {
+ if (tasksDto.getTitle() == null) {
+ throw new StatusTaskNotBeNullException(STATUS_NOT_BE_NULL.getEnumDescription());
+ } else if (tasksDto.getDateCalendar() == null) {
+ throw new DataCalendarNotBeNullException(DATE_CALENDAR_NOT_BE_NULL.getEnumDescription());
+ }
+ Task task = taskMapper.convertDtoToTasks(tasksDto);
+ task.setId(null);
+ task = taskRepository.save(task);
+ log.info("create");
+ return taskMapper.convertTasksToDto(task);
+ }
+
+ @CachePut(cacheNames = "taskServiceCache", key = "#tasksDto.id")
+ @Transactional
+ @Override
+ public Optional changeTasks(TaskDto tasksDto) throws EntityNotFoundException {
+ Task taskFromDB = taskRepository.findById(tasksDto.getId());
+ if (taskFromDB == null) {
+ throw new EntityNotFoundException(TASK_NOT_FOUND_BY_ID.getEnumDescription());
+ }
+ taskMapper.compareTaskAndDto(tasksDto, taskFromDB);
+ taskRepository.update(taskFromDB);
+ if (taskFromDB.getId() == null) {
+ return Optional.empty();
+ }
+ log.info("change");
+ return Optional.of(taskMapper.convertTasksToDto(taskFromDB));
+ }
+
+ @Cacheable(cacheNames = "taskServiceCache", key = "#result.?[].id")
+ @Transactional
+ @Override
+ public Optional> getTasksByTitle(String taskTitle, Integer offset, Integer limit) {
+ StringBuffer localStringBuffer = new StringBuffer(taskTitle);
+ char[] chars = taskTitle.toCharArray();
+ if (chars[ZERO] != PERCENT_CHAR_PRIMITIVE) {
+ localStringBuffer.insert(ZERO, PERCENT);
+ }
+ if (chars[chars.length - ONE] != PERCENT_CHAR_PRIMITIVE) {
+ localStringBuffer.append(PERCENT);
+ }
+ Optional> optionalTaskList = taskRepository.findAllByTitle(localStringBuffer.toString(), offset, limit);
+ if (optionalTaskList.isPresent() && !optionalTaskList.get().isEmpty()) {
+ return Optional.of(taskMapper.transferListTasksToDto(optionalTaskList.get()));
+ }
+ log.info("get");
+ return Optional.empty();
+ }
+
+ @CacheEvict(cacheNames = "taskServiceCache", key = "#idTasks")
+ @Transactional
+ @Override
+ public boolean deleteTasks(Long idTasks) {
+ boolean taskExist = taskRepository.existsById(idTasks);
+ if (taskExist) {
+ taskRepository.deleteById(idTasks);
+ log.info("delete");
+ return true;
+ }
+ log.info("delete");
+ return false;
+ }
+}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 5c3e5461..4ba605bd 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -1 +1,13 @@
spring.application.name=SpringToDo
+springdoc.swagger-ui.path=swagger-ui.html
+spring.datasource.url=jdbc:postgresql://${BASEPROJECT_DATABASE_HOST}:${BASEPROJECT_DATABASE_PORT}/${BASEPROJECT_DATABASE_NAME}
+spring.datasource.username=${BASEPROJECT_DATABASE_USERNAME}
+spring.datasource.password=${BASEPROJECT_DATABASE_PASSWORD}
+spring.datasource.driverClassName=org.postgresql.Driver
+spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
+spring.liquibase.change-log=classpath:db/changelog/db.changelog-master.yaml
+spring.liquibase.enabled=true
+spring.cache.type=redis
+spring.data.redis.host=localhost
+spring.data.redis.port=6379
+spring.cache.cache-names=taskServiceCache
\ No newline at end of file
diff --git a/src/main/resources/db/changelog/V1.0.0-create-table-changelog.xml b/src/main/resources/db/changelog/V1.0.0-create-table-changelog.xml
new file mode 100644
index 00000000..2d03ef9e
--- /dev/null
+++ b/src/main/resources/db/changelog/V1.0.0-create-table-changelog.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/db/changelog/V1.0.1-testdata-in-tables-changelog.xml b/src/main/resources/db/changelog/V1.0.1-testdata-in-tables-changelog.xml
new file mode 100644
index 00000000..2c133a8a
--- /dev/null
+++ b/src/main/resources/db/changelog/V1.0.1-testdata-in-tables-changelog.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/db/changelog/db.changelog-master.yaml b/src/main/resources/db/changelog/db.changelog-master.yaml
new file mode 100644
index 00000000..edcf8fb3
--- /dev/null
+++ b/src/main/resources/db/changelog/db.changelog-master.yaml
@@ -0,0 +1,5 @@
+databaseChangeLog:
+ - include:
+ file: db/changelog/V1.0.0-create-table-changelog.xml
+ - include:
+ file: db/changelog/V1.0.1-testdata-in-tables-changelog.xml
\ No newline at end of file
diff --git a/src/test/java/com/emobile/springtodo/controllers/TaskControllerTest.java b/src/test/java/com/emobile/springtodo/controllers/TaskControllerTest.java
new file mode 100644
index 00000000..fbdb555a
--- /dev/null
+++ b/src/test/java/com/emobile/springtodo/controllers/TaskControllerTest.java
@@ -0,0 +1,129 @@
+package com.emobile.springtodo.controllers;
+
+import com.emobile.springtodo.dto.TaskDto;
+import com.emobile.springtodo.services.TaskService;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+
+import java.time.LocalDateTime;
+import java.util.Collections;
+import java.util.Optional;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+
+@SpringBootTest
+@AutoConfigureMockMvc
+class TaskControllerTest {
+
+ @Autowired
+ private MockMvc mockMvc;
+
+ @MockBean
+ private TaskService taskService;
+
+ @Autowired
+ private ObjectMapper objectMapper;
+
+ @DisplayName("Проверка POST /task/create, при валидном запросе")
+ @Test
+ void createTask_ValidRequest_ReturnsCreatedTask() throws Exception {
+ TaskDto taskDto = new TaskDto();
+ taskDto.setTitle("Valid Task");
+ taskDto.setStatus(true);
+ taskDto.setDateCalendar(LocalDateTime.now());
+
+ when(taskService.createTasks(any())).thenReturn(taskDto);
+
+ mockMvc.perform(post("/task/create")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(taskDto)))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.title").value("Valid Task"));
+ }
+
+ @DisplayName("Проверка POST /task/create, при невалидном запросе")
+ @Test
+ void createTask_InvalidRequest_ReturnsBadRequest() throws Exception {
+ mockMvc.perform(post("/task/create")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content("{\"description\": \"Invalid Task\"}"))
+ .andExpect(status().isBadRequest());
+ }
+
+ @DisplayName("Проверка GET /task/list/{title}, при валидном запросе")
+ @Test
+ void getTasksByTitle_ExistingTitle_ReturnsTasks() throws Exception {
+ when(taskService.getTasksByTitle("Test", 0, 10))
+ .thenReturn(Optional.of(Collections.singletonList(new TaskDto())));
+
+ mockMvc.perform(get("/task/list/Test?offset=0&limit=10"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$").isArray());
+ }
+
+ @DisplayName("Проверка GET /task/list/{title}, при невалидном запросе")
+ @Test
+ void getTasksByTitle_NonExistingTitle_ReturnsBadRequest() throws Exception {
+ when(taskService.getTasksByTitle("Unknown", 0, 10))
+ .thenReturn(Optional.empty());
+
+ mockMvc.perform(get("/task/list/Unknown?offset=0&limit=10"))
+ .andExpect(status().isBadRequest());
+ }
+
+ @DisplayName("Проверка GET /task/list/{title}, при валидном запросе")
+ @Test
+ void updateTask_ValidRequest_ReturnsUpdatedTask() throws Exception {
+ TaskDto taskDto = new TaskDto();
+ taskDto.setId(1L);
+ taskDto.setTitle("Updated Task");
+
+ when(taskService.changeTasks(any())).thenReturn(Optional.of(taskDto));
+
+ mockMvc.perform(put("/task/update-tasks")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(taskDto)))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.title").value("Updated Task"));
+ }
+
+ @DisplayName("Проверка GET /task/list/{title}, при невалидном запросе")
+ @Test
+ void updateTask_NonExistingTask_ReturnsBadRequest() throws Exception {
+ when(taskService.changeTasks(any())).thenReturn(Optional.empty());
+
+ mockMvc.perform(put("/task/update-tasks")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(new TaskDto())))
+ .andExpect(status().isBadRequest());
+ }
+
+ @DisplayName("Проверка DELETE /task/delete/{id}, при валидном запросе")
+ @Test
+ void deleteTask_ExistingId_ReturnsSuccess() throws Exception {
+ when(taskService.deleteTasks(1L)).thenReturn(true);
+
+ mockMvc.perform(delete("/task/delete/1"))
+ .andExpect(status().isOk())
+ .andExpect(content().string("Entity was deleted"));
+ }
+
+ @DisplayName("Проверка DELETE /task/delete/{id}, при невалидном запросе")
+ @Test
+ void deleteTask_NonExistingId_ReturnsNoContent() throws Exception {
+ when(taskService.deleteTasks(999L)).thenReturn(false);
+
+ mockMvc.perform(delete("/task/delete/999"))
+ .andExpect(status().isNoContent());
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/emobile/springtodo/repositories/TaskRepositoryTest.java b/src/test/java/com/emobile/springtodo/repositories/TaskRepositoryTest.java
new file mode 100644
index 00000000..991b9346
--- /dev/null
+++ b/src/test/java/com/emobile/springtodo/repositories/TaskRepositoryTest.java
@@ -0,0 +1,78 @@
+package com.emobile.springtodo.repositories;
+
+import com.emobile.springtodo.entities.Task;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.core.PreparedStatementCreator;
+import org.springframework.jdbc.core.RowMapper;
+import org.springframework.jdbc.support.GeneratedKeyHolder;
+
+import java.time.LocalDateTime;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Optional;
+
+@ExtendWith(MockitoExtension.class)
+class TaskRepositoryTest {
+
+ @Mock
+ private JdbcTemplate jdbcTemplate;
+
+ @InjectMocks
+ private TaskRepositoryImpl taskRepository;
+
+ @DisplayName("Проверка метода existsById")
+ @Test
+ void existsById_WhenExists_ReturnsTrue() {
+ Mockito.when(jdbcTemplate.queryForObject(Mockito.anyString(), Mockito.eq(Integer.class), Mockito.anyLong()))
+ .thenReturn(1);
+
+ Assertions.assertTrue(taskRepository.existsById(1L));
+ Mockito.verify(jdbcTemplate).queryForObject(
+ "SELECT COUNT(*) FROM tasks_entity WHERE id = ?",
+ Integer.class, 1L
+ );
+ }
+
+ @DisplayName("Проверка метода save")
+ @Test
+ void save_ValidTask_GeneratesId() {
+ Task task = new Task();
+ task.setTitle("Test");
+ task.setStatus(true);
+ task.setDateCalendar(LocalDateTime.now());
+
+ Mockito.when(jdbcTemplate.update(Mockito.any(PreparedStatementCreator.class),
+ Mockito.any(GeneratedKeyHolder.class)))
+ .thenAnswer(inv -> {
+ GeneratedKeyHolder keyHolder = inv.getArgument(1);
+ keyHolder.getKeyList().add(new HashMap<>() {{
+ put("id", 1L);
+ }});
+ return 1;
+ });
+
+ Task result = taskRepository.save(task);
+ Assertions.assertEquals(1L, result.getId());
+ }
+
+ @DisplayName("Проверка метода findAll")
+ @Test
+ void findAllByTitle_ValidTitle_ReturnsTasks() {
+ Task task = new Task();
+ Mockito.when(jdbcTemplate.query(Mockito.anyString(), Mockito.any(RowMapper.class), Mockito.any(),
+ Mockito.anyInt(), Mockito.anyInt()))
+ .thenReturn(List.of(task));
+
+ Optional> result = taskRepository.findAllByTitle("test", 0, 10);
+ Assertions.assertTrue(result.isPresent());
+ Assertions.assertEquals(1, result.get().size());
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/emobile/springtodo/services/TaskServicesTest.java b/src/test/java/com/emobile/springtodo/services/TaskServicesTest.java
new file mode 100644
index 00000000..c1ad5e6b
--- /dev/null
+++ b/src/test/java/com/emobile/springtodo/services/TaskServicesTest.java
@@ -0,0 +1,112 @@
+package com.emobile.springtodo.services;
+
+import com.emobile.springtodo.dto.TaskDto;
+import com.emobile.springtodo.entities.Task;
+import com.emobile.springtodo.exceptions.DataCalendarNotBeNullException;
+import com.emobile.springtodo.exceptions.EntityNotFoundException;
+import com.emobile.springtodo.exceptions.StatusTaskNotBeNullException;
+import com.emobile.springtodo.mappers.TaskMapper;
+import com.emobile.springtodo.repositories.TaskRepository;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.time.LocalDateTime;
+import java.util.Optional;
+
+@ExtendWith(MockitoExtension.class)
+class TaskServicesTest {
+
+ @Mock
+ private TaskMapper taskMapper;
+
+ @Mock
+ private TaskRepository taskRepository;
+
+ @InjectMocks
+ private TaskServicesImpl taskServices;
+
+ private TaskDto taskDto;
+ private Task task;
+
+ @BeforeEach
+ void setUp() {
+ taskDto = new TaskDto(1L, "Test Task", "Description", true, LocalDateTime.now());
+ task = new Task(1L, "Test Task", "Description", true, LocalDateTime.now());
+ }
+
+ @DisplayName("Проверка возвращаемого значения метода createTasks")
+ @Test
+ void createTasks_ShouldReturnCreatedTask() throws StatusTaskNotBeNullException, DataCalendarNotBeNullException {
+ Mockito.when(taskMapper.convertDtoToTasks(Mockito.any())).thenReturn(task);
+ Mockito.when(taskRepository.save(Mockito.any())).thenReturn(task);
+ Mockito.when(taskMapper.convertTasksToDto(Mockito.any())).thenReturn(taskDto);
+
+ TaskDto createdTask = taskServices.createTasks(taskDto);
+
+ Assertions.assertNotNull(createdTask);
+ Assertions.assertEquals("Test Task", createdTask.getTitle());
+ Mockito.verify(taskRepository, Mockito.times(1)).save(Mockito.any(Task.class));
+ }
+
+ @DisplayName("Проверка выбрасываемого эксепешена StatusTaskNotBeNullException метода createTasks")
+ @Test
+ void createTasks_ShouldThrowStatusTaskNotBeNullException() {
+ taskDto.setTitle(null);
+ Assertions.assertThrows(StatusTaskNotBeNullException.class, () -> taskServices.createTasks(taskDto));
+ }
+
+ @DisplayName("Проверка выбрасываемого эксепешена DataCalendarNotBeNullException метода createTasks")
+ @Test
+ void createTasks_ShouldThrowDataCalendarNotBeNullException() {
+ taskDto.setDateCalendar(null);
+ Assertions.assertThrows(DataCalendarNotBeNullException.class, () -> taskServices.createTasks(taskDto));
+ }
+
+ @DisplayName("Проверка возвращаемого значения метода changeTasks")
+ @Test
+ void changeTasks_ShouldReturnUpdatedTask() throws EntityNotFoundException {
+ Mockito.when(taskRepository.findById(taskDto.getId())).thenReturn(task);
+ Mockito.when(taskMapper.convertTasksToDto(Mockito.any())).thenReturn(taskDto);
+
+ Optional updatedTask = taskServices.changeTasks(taskDto);
+
+ Assertions.assertTrue(updatedTask.isPresent());
+ Mockito.verify(taskRepository, Mockito.times(1)).update(Mockito.any(Task.class));
+ }
+
+ @DisplayName("роверка выбрасываемого эксепешена EntityNotFoundException метода changeTasks")
+ @Test
+ void changeTasks_ShouldThrowEntityNotFoundException() {
+ Mockito.when(taskRepository.findById(Mockito.anyLong())).thenReturn(null);
+ Assertions.assertThrows(EntityNotFoundException.class, () -> taskServices.changeTasks(taskDto));
+ }
+
+ @DisplayName("Проверка возвращаемого значения метода deleteTasks, сценарий, когда true")
+ @Test
+ void deleteTasks_ShouldReturnTrue() {
+ Mockito.when(taskRepository.existsById(Mockito.anyLong())).thenReturn(true);
+ Mockito.doNothing().when(taskRepository).deleteById(Mockito.anyLong());
+
+ boolean result = taskServices.deleteTasks(1L);
+
+ Assertions.assertTrue(result);
+ Mockito.verify(taskRepository, Mockito.times(1)).deleteById(Mockito.anyLong());
+ }
+
+ @DisplayName("Проверка возвращаемого значения метода deleteTasks, сценарий, когда false")
+ @Test
+ void deleteTasks_ShouldReturnFalse() {
+ Mockito.when(taskRepository.existsById(Mockito.anyLong())).thenReturn(false);
+ boolean result = taskServices.deleteTasks(1L);
+
+ Assertions.assertFalse(result);
+ Mockito.verify(taskRepository, Mockito.never()).deleteById(Mockito.anyLong());
+ }
+}