From a94b84f4d1656f3fb9389af09e78e9f745aca86e Mon Sep 17 00:00:00 2001 From: vsirik Date: Mon, 3 Feb 2025 05:50:57 +0700 Subject: [PATCH 1/9] commit --- README.md | 3 +- pom.xml | 35 ++- .../annotations/FilterResponse.java | 12 ++ .../springtodo/config/OpenApiConfig.java | 16 ++ .../HandlerExceptionController.java | 203 ++++++++++++++++++ .../controllers/TaskController.java | 142 ++++++++++++ .../com/emobile/springtodo/dto/TaskDto.java | 46 ++++ .../com/emobile/springtodo/entities/Task.java | 26 +++ .../exceptions/ApiErrorResponse.java | 24 +++ .../exceptions/DescriptionUserExeption.java | 23 ++ .../exceptions/EntityNotFoundException.java | 7 + .../springtodo/exceptions/Violation.java | 13 ++ .../springtodo/mappers/TaskMapper.java | 42 ++++ .../springtodo/mappers/TaskMapperImpl.java | 88 ++++++++ .../springtodo/other/ConstantsClass.java | 18 ++ .../repositories/JdbcRepository.java | 13 ++ .../repositories/TaskRepository.java | 13 ++ .../repositories/TaskRepositoryImpl.java | 126 +++++++++++ .../springtodo/services/TaskService.java | 49 +++++ .../springtodo/services/TaskServicesImpl.java | 64 ++++++ src/main/resources/application.properties | 1 + 21 files changed, 962 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/emobile/springtodo/annotations/FilterResponse.java create mode 100644 src/main/java/com/emobile/springtodo/config/OpenApiConfig.java create mode 100644 src/main/java/com/emobile/springtodo/controllers/HandlerExceptionController.java create mode 100644 src/main/java/com/emobile/springtodo/controllers/TaskController.java create mode 100644 src/main/java/com/emobile/springtodo/dto/TaskDto.java create mode 100644 src/main/java/com/emobile/springtodo/entities/Task.java create mode 100644 src/main/java/com/emobile/springtodo/exceptions/ApiErrorResponse.java create mode 100644 src/main/java/com/emobile/springtodo/exceptions/DescriptionUserExeption.java create mode 100644 src/main/java/com/emobile/springtodo/exceptions/EntityNotFoundException.java create mode 100644 src/main/java/com/emobile/springtodo/exceptions/Violation.java create mode 100644 src/main/java/com/emobile/springtodo/mappers/TaskMapper.java create mode 100644 src/main/java/com/emobile/springtodo/mappers/TaskMapperImpl.java create mode 100644 src/main/java/com/emobile/springtodo/other/ConstantsClass.java create mode 100644 src/main/java/com/emobile/springtodo/repositories/JdbcRepository.java create mode 100644 src/main/java/com/emobile/springtodo/repositories/TaskRepository.java create mode 100644 src/main/java/com/emobile/springtodo/repositories/TaskRepositoryImpl.java create mode 100644 src/main/java/com/emobile/springtodo/services/TaskService.java create mode 100644 src/main/java/com/emobile/springtodo/services/TaskServicesImpl.java diff --git a/README.md b/README.md index 60f40028..098fe96d 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
diff --git a/pom.xml b/pom.xml index 03bc446d..3f3655d9 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,14 @@ spring-boot-starter-test test + + + + org.springframework.boot + spring-boot-starter-jdbc + 3.4.2 + + @@ -36,6 +61,14 @@ org.springframework.boot spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + diff --git a/src/main/java/com/emobile/springtodo/annotations/FilterResponse.java b/src/main/java/com/emobile/springtodo/annotations/FilterResponse.java new file mode 100644 index 00000000..954aaa22 --- /dev/null +++ b/src/main/java/com/emobile/springtodo/annotations/FilterResponse.java @@ -0,0 +1,12 @@ +package com.emobile.springtodo.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@interface FilterResponse { + String filterName() default ""; +} 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..e2682111 --- /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 = "TaskDto Management System", + description = "TO-DO List", + 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..80ab3b2b --- /dev/null +++ b/src/main/java/com/emobile/springtodo/controllers/HandlerExceptionController.java @@ -0,0 +1,203 @@ +package com.emobile.springtodo.controllers; + + +import com.emobile.springtodo.exceptions.ApiErrorResponse; +import com.emobile.springtodo.exceptions.EntityNotFoundException; +import com.emobile.springtodo.exceptions.Violation; +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.LINE_FEED; + +/** + *
+ *     Контроллер, обрабатывающий все эксепшены, котоыре могут быть выброшены в процессе работы приложения
+ * 
+ */ +@ControllerAdvice +@Slf4j +public class HandlerExceptionController { + + /** + * Обработчик ExecutorNotFoundException + */ + @ExceptionHandler(EntityNotFoundException.class) + protected ResponseEntity handleExecutorNotFoundExeption(EntityNotFoundException e, HttpServletRequest request) { + log.error(GENERATION_ERROR.getEnumDescription() + e.getClass() + LINE_FEED + e.getMessage() + LINE_FEED + e); + + ApiErrorResponse apiErrorResponse = buildApiErrorResponse( + HttpStatus.NOT_FOUND, + e.getMessage(), + request.getRequestURI(), + null + ); + + return new ResponseEntity<>(apiErrorResponse, HttpStatus.NOT_FOUND); + } + + /** + * Обработчик ConstraintViolationException + */ + @ExceptionHandler(ConstraintViolationException.class) + protected ResponseEntity handleConstraintViolationException(ConstraintViolationException e, + HttpServletRequest request) { + log.error(GENERATION_ERROR.getEnumDescription() + e.getClass() + LINE_FEED + e.getMessage() + LINE_FEED + + 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(GENERATION_ERROR.getEnumDescription() + e.getClass() + LINE_FEED + e.getMessage() + LINE_FEED + + 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(GENERATION_ERROR.getEnumDescription() + e.getClass() + LINE_FEED + e.getMessage() + LINE_FEED + + 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(GENERATION_ERROR.getEnumDescription() + e.getClass() + LINE_FEED + e.getMessage() + LINE_FEED + + 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(GENERATION_ERROR.getEnumDescription() + e.getClass() + LINE_FEED + e.getMessage() + LINE_FEED + + 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("Возникла непредвиденная ошибка " + e.getClass() + LINE_FEED + e.getMessage() + LINE_FEED + + 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("Возникла непредвиденная ошибка " + e.getClass() + LINE_FEED + e.getMessage() + LINE_FEED + + 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..94cc8ca3 --- /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.EntityNotFoundException; +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) { + 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/gen-info/executor/{executorEmail}", consumes = MediaType.ALL_VALUE) + public ResponseEntity> getTasksByTitle( + @PathVariable("executorEmail") @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); + if (optionalExecutorTasksDtoList.isPresent()) { + return ResponseEntity.ok(optionalExecutorTasksDtoList.get()); + } + return 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); + if (newTasksDto.isPresent()) { + return ResponseEntity.ok(newTasksDto.get()); + } + return ResponseEntity.badRequest().build(); + } + + /** + * Эндпоинд DELETE для удаления задач + * + * @param idTasks - id задачи для удаления + * @return {@link ResponseEntity} + */ + @Operation(summary = "Удалить задачу") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Задача успешно отредактирована", content = @Content(examples = @ExampleObject(value = "{\nСущность была удалена\n}"))), + @ApiResponse(responseCode = "400", description = "Не удалось произвести удаление задачи", content = @Content) + }) + @DeleteMapping(value = "/task/delete/{id}", consumes = MediaType.ALL_VALUE, produces = MediaType.TEXT_PLAIN_VALUE) + public ResponseEntity deleteTasks(@PathVariable("id") @Parameter(description = "ID задачи") @NotNull(message = "ID задачи не может быть null") + Long idTasks) { + log.info("Удаление задачи по id, метод DELETE" + idTasks); + taskService.deleteTasks(idTasks); + return ResponseEntity.ok(IS_DELETE); + } +} 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..cc40ad44 --- /dev/null +++ b/src/main/java/com/emobile/springtodo/dto/TaskDto.java @@ -0,0 +1,46 @@ +package com.emobile.springtodo.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.*; +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") + @NotNull(message = "Статус задачи не может быть пуст") + private Boolean status; + + @Schema(description = "Дата задачи в календаре", example = "2020-01-01T00:01:10") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss") + @NotNull(message = "Дата задачи в календаре не может быть пустой") + 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/DescriptionUserExeption.java b/src/main/java/com/emobile/springtodo/exceptions/DescriptionUserExeption.java new file mode 100644 index 00000000..72ebf2c3 --- /dev/null +++ b/src/main/java/com/emobile/springtodo/exceptions/DescriptionUserExeption.java @@ -0,0 +1,23 @@ +package com.emobile.springtodo.exceptions; + +/** + *
+ *     Enum с описанием для кастомных ошибок, которые могут выбрасываться в некоторых частях приложения
+ * 
+ */ +public enum DescriptionUserExeption { + + GENERATION_ERROR("Возникла ошибка в системе: "), + + TASK_NOT_FOUND_BY_ID("Задача по переданному ID не найдена"); + + 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..43f7dd01 --- /dev/null +++ b/src/main/java/com/emobile/springtodo/exceptions/EntityNotFoundException.java @@ -0,0 +1,7 @@ +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/Violation.java b/src/main/java/com/emobile/springtodo/exceptions/Violation.java new file mode 100644 index 00000000..37b4766e --- /dev/null +++ b/src/main/java/com/emobile/springtodo/exceptions/Violation.java @@ -0,0 +1,13 @@ +package com.emobile.springtodo.exceptions; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class Violation { + + private final String fieldName; + private final 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..a7ac6c14 --- /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 } в {@link } + * + * @param tasksDto DTO объекта задачи + * @return Сконвертированный объект задачи + */ + Task convertDtoToTasks(TaskDto tasksDto); + + /** + * Метод, конвертирующий {@link } в {@link } + * + * @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..dad3554c --- /dev/null +++ b/src/main/java/com/emobile/springtodo/other/ConstantsClass.java @@ -0,0 +1,18 @@ +package com.emobile.springtodo.other; + +/** + *
+ *     Класс с константами
+ * 
+ */ +public class ConstantsClass { + + public static final String IS_DELETE = "Сущность была удалена"; + public static final String ID_FIELD = "id"; + public static final String TITLE_FIELD = "title_tasks"; + public static final String DESCRIPTION_FIELD = "description_tasks"; + public static final String STATUS_FIELD = "status_tasks"; + public static final String DATA_CALENDAR_FIELD = "dateCalendar_tasks"; + public static final String LINE_FEED = "\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..e885ad64 --- /dev/null +++ b/src/main/java/com/emobile/springtodo/repositories/JdbcRepository.java @@ -0,0 +1,13 @@ +package com.emobile.springtodo.repositories; + +public interface JdbcRepository { + + R save(R entity); + + R update(R newEntity); + + R findById(T 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..0b2bfdc2 --- /dev/null +++ b/src/main/java/com/emobile/springtodo/repositories/TaskRepository.java @@ -0,0 +1,13 @@ +package com.emobile.springtodo.repositories; + +import com.emobile.springtodo.entities.Task; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +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..fb302935 --- /dev/null +++ b/src/main/java/com/emobile/springtodo/repositories/TaskRepositoryImpl.java @@ -0,0 +1,126 @@ +package com.emobile.springtodo.repositories; + +import com.emobile.springtodo.entities.Task; +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.*; + +@Repository +public class TaskRepositoryImpl implements TaskRepository { + + private final JdbcTemplate jdbcTemplate; + + @Autowired + public TaskRepositoryImpl(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + @Override + public Task save(Task entity) { + String sql = "INSERT INTO tasks_entity (title_tasks, description_tasks, status_tasks, dateCalendar_tasks) " + + "VALUES (?, ?, ?, ?)"; + commonUpdate(sql, entity); + return entity; + } + + @Override + public Task update(Task newEntity) { + String sql = "UPDATE tasks_entity SET title_tasks = ?, description_tasks = ?, status_tasks = ?, dateCalendar_tasks = ? WHERE id = ? "; + commonUpdate(sql, newEntity); + return newEntity; + } + + @Override + public Task findById(Long id) { + String sql = "SELECT id, title_tasks, description_tasks, status_tasks, dateCalendar_tasks " + + "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_tasks, description_tasks, status_tasks, dateCalendar_tasks " + + "FROM tasks_entity WHERE title_tasks = ? 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); + } + + 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..9d0ffcd1 --- /dev/null +++ b/src/main/java/com/emobile/springtodo/services/TaskService.java @@ -0,0 +1,49 @@ +package com.emobile.springtodo.services; + +import com.emobile.springtodo.dto.TaskDto; +import com.emobile.springtodo.exceptions.EntityNotFoundException; + +import java.util.List; +import java.util.Optional; + +/** + *
+ *     Интерфейс для работы с сущностью Tasks
+ * 
+ */ +public interface TaskService { + + /** + * Метод для создания задачи + * + * @param tasksDto DTO объекта задачи, содержащий данные для создания + * @return Созданный объект-задача в виде {@link } + */ + TaskDto createTasks(TaskDto tasksDto); + + /** + * Метод для редактирования задачи + * + * @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 задачи, которую нужно удалить + */ + void 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..b1616a93 --- /dev/null +++ b/src/main/java/com/emobile/springtodo/services/TaskServicesImpl.java @@ -0,0 +1,64 @@ +package com.emobile.springtodo.services; + +import com.emobile.springtodo.dto.TaskDto; +import com.emobile.springtodo.entities.Task; +import com.emobile.springtodo.exceptions.EntityNotFoundException; +import com.emobile.springtodo.mappers.TaskMapper; +import com.emobile.springtodo.repositories.TaskRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; + +import static com.emobile.springtodo.exceptions.DescriptionUserExeption.TASK_NOT_FOUND_BY_ID; + +@Service +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; + } + + + @Override + public TaskDto createTasks(TaskDto tasksDto) { + Task task = taskMapper.convertDtoToTasks(tasksDto); + task.setId(null); + task = taskRepository.save(task); + return taskMapper.convertTasksToDto(task); + } + + @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(); + } + return Optional.of(taskMapper.convertTasksToDto(taskFromDB)); + } + + @Override + public Optional> getTasksByTitle(String tasksName, Integer offset, Integer limit) { + Optional> optionalTaskList = taskRepository.findAllByTitle(tasksName, offset, limit); + if (optionalTaskList.isPresent()) { + return Optional.of(taskMapper.transferListTasksToDto(optionalTaskList.get())); + } + return Optional.empty(); + } + + @Override + public void deleteTasks(Long idTasks) { + taskRepository.deleteById(idTasks); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 5c3e5461..8ed7c418 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,2 @@ spring.application.name=SpringToDo +springdoc.swagger-ui.path=swagger-ui.html From fdd5e1cdb9bfd4cfa45ba853c595bad1bf312144 Mon Sep 17 00:00:00 2001 From: vsirik Date: Tue, 4 Feb 2025 08:05:47 +0700 Subject: [PATCH 2/9] docker --- .env | 5 ++ .mvn/wrapper/maven-wrapper.properties | 19 ++++++++ Dockerfile | 20 ++++++++ docker-compose.yaml | 47 +++++++++++++++++++ pom.xml | 13 +++++ .../springtodo/other/ConstantsClass.java | 8 ++-- .../repositories/TaskRepositoryImpl.java | 12 ++--- src/main/resources/application.properties | 7 +++ .../V1.0.0-create-table-changelog.xml | 27 +++++++++++ .../V1.0.1-testdata-in-tables-changelog.xml | 22 +++++++++ .../db/changelog/db.changelog-master.yaml | 5 ++ 11 files changed, 175 insertions(+), 10 deletions(-) create mode 100644 .env create mode 100644 .mvn/wrapper/maven-wrapper.properties create mode 100644 Dockerfile create mode 100644 docker-compose.yaml create mode 100644 src/main/resources/db/changelog/V1.0.0-create-table-changelog.xml create mode 100644 src/main/resources/db/changelog/V1.0.1-testdata-in-tables-changelog.xml create mode 100644 src/main/resources/db/changelog/db.changelog-master.yaml 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/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 00000000..7099b20f --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,47 @@ +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 + depends_on: + - postgres +volumes: + postgres_data: \ No newline at end of file diff --git a/pom.xml b/pom.xml index 3f3655d9..af2372ff 100644 --- a/pom.xml +++ b/pom.xml @@ -54,6 +54,19 @@ 3.4.2 + + + org.liquibase + liquibase-core + 4.31.0 + + + + org.postgresql + postgresql + runtime + + diff --git a/src/main/java/com/emobile/springtodo/other/ConstantsClass.java b/src/main/java/com/emobile/springtodo/other/ConstantsClass.java index dad3554c..10901232 100644 --- a/src/main/java/com/emobile/springtodo/other/ConstantsClass.java +++ b/src/main/java/com/emobile/springtodo/other/ConstantsClass.java @@ -9,10 +9,10 @@ public class ConstantsClass { public static final String IS_DELETE = "Сущность была удалена"; public static final String ID_FIELD = "id"; - public static final String TITLE_FIELD = "title_tasks"; - public static final String DESCRIPTION_FIELD = "description_tasks"; - public static final String STATUS_FIELD = "status_tasks"; - public static final String DATA_CALENDAR_FIELD = "dateCalendar_tasks"; + 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 = "dateCalendar"; public static final String LINE_FEED = "\n"; } diff --git a/src/main/java/com/emobile/springtodo/repositories/TaskRepositoryImpl.java b/src/main/java/com/emobile/springtodo/repositories/TaskRepositoryImpl.java index fb302935..2c3c0a67 100644 --- a/src/main/java/com/emobile/springtodo/repositories/TaskRepositoryImpl.java +++ b/src/main/java/com/emobile/springtodo/repositories/TaskRepositoryImpl.java @@ -28,7 +28,7 @@ public TaskRepositoryImpl(JdbcTemplate jdbcTemplate) { @Override public Task save(Task entity) { - String sql = "INSERT INTO tasks_entity (title_tasks, description_tasks, status_tasks, dateCalendar_tasks) " + + String sql = "INSERT INTO tasks_entity (title, description, status, dateCalendar) " + "VALUES (?, ?, ?, ?)"; commonUpdate(sql, entity); return entity; @@ -36,23 +36,23 @@ public Task save(Task entity) { @Override public Task update(Task newEntity) { - String sql = "UPDATE tasks_entity SET title_tasks = ?, description_tasks = ?, status_tasks = ?, dateCalendar_tasks = ? WHERE id = ? "; + String sql = "UPDATE tasks_entity SET title = ?, description = ?, status = ?, dateCalendar = ? WHERE id = ? "; commonUpdate(sql, newEntity); return newEntity; } @Override public Task findById(Long id) { - String sql = "SELECT id, title_tasks, description_tasks, status_tasks, dateCalendar_tasks " + + String sql = "SELECT id, title, description, status, dateCalendar " + "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_tasks, description_tasks, status_tasks, dateCalendar_tasks " + - "FROM tasks_entity WHERE title_tasks = ? ORDER BY id LIMIT ? OFFSET ?"; - return Optional.of(jdbcTemplate.query(sql, rowMapper(), title, limit, offset)); + String sql = "SELECT id, title, description, status, dateCalendar " + + "FROM tasks_entity WHERE title = ? ORDER BY id LIMIT ? OFFSET ?"; + return Optional.of(jdbcTemplate.query(sql, rowMapper(), title, offset, limit)); } @Override diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 8ed7c418..855d4d3c 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,2 +1,9 @@ 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 \ 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..c3f10524 --- /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..deb37bae --- /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 From 02acac0e8f0a682b69b6e3f485f2f52d21104d79 Mon Sep 17 00:00:00 2001 From: vsirik Date: Tue, 4 Feb 2025 23:43:17 +0700 Subject: [PATCH 3/9] Spring cache --- README.md | 19 +++++++ .../annotations/FilterResponse.java | 12 ---- .../HandlerExceptionController.java | 32 ++++++++++- .../controllers/TaskController.java | 20 ++++--- .../com/emobile/springtodo/dto/TaskDto.java | 7 ++- .../DataCalendarNotBeNullException.java | 10 ++++ .../exceptions/DescriptionUserExeption.java | 6 +- .../exceptions/EntityNotFoundException.java | 3 + .../StatusTaskNotBeNullException.java | 10 ++++ .../springtodo/mappers/TaskMapper.java | 4 +- .../springtodo/other/ConstantsClass.java | 6 +- .../repositories/JdbcRepository.java | 37 +++++++++++++ .../repositories/TaskRepository.java | 4 +- .../repositories/TaskRepositoryImpl.java | 25 +++++++-- .../springtodo/services/TaskService.java | 9 ++- .../springtodo/services/TaskServicesImpl.java | 55 ++++++++++++++++--- .../V1.0.0-create-table-changelog.xml | 2 +- .../V1.0.1-testdata-in-tables-changelog.xml | 4 +- 18 files changed, 215 insertions(+), 50 deletions(-) delete mode 100644 src/main/java/com/emobile/springtodo/annotations/FilterResponse.java create mode 100644 src/main/java/com/emobile/springtodo/exceptions/DataCalendarNotBeNullException.java create mode 100644 src/main/java/com/emobile/springtodo/exceptions/StatusTaskNotBeNullException.java diff --git a/README.md b/README.md index 098fe96d..1237b0c2 100644 --- a/README.md +++ b/README.md @@ -19,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/src/main/java/com/emobile/springtodo/annotations/FilterResponse.java b/src/main/java/com/emobile/springtodo/annotations/FilterResponse.java deleted file mode 100644 index 954aaa22..00000000 --- a/src/main/java/com/emobile/springtodo/annotations/FilterResponse.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.emobile.springtodo.annotations; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.METHOD) -@interface FilterResponse { - String filterName() default ""; -} diff --git a/src/main/java/com/emobile/springtodo/controllers/HandlerExceptionController.java b/src/main/java/com/emobile/springtodo/controllers/HandlerExceptionController.java index 80ab3b2b..8ae09886 100644 --- a/src/main/java/com/emobile/springtodo/controllers/HandlerExceptionController.java +++ b/src/main/java/com/emobile/springtodo/controllers/HandlerExceptionController.java @@ -1,9 +1,7 @@ package com.emobile.springtodo.controllers; -import com.emobile.springtodo.exceptions.ApiErrorResponse; -import com.emobile.springtodo.exceptions.EntityNotFoundException; -import com.emobile.springtodo.exceptions.Violation; +import com.emobile.springtodo.exceptions.*; import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.ConstraintViolation; import jakarta.validation.ConstraintViolationException; @@ -51,6 +49,34 @@ protected ResponseEntity handleExecutorNotFoundExeption(Entity return new ResponseEntity<>(apiErrorResponse, HttpStatus.NOT_FOUND); } + @ExceptionHandler(DataCalendarNotBeNullException.class) + protected ResponseEntity handleExecutorNotFoundExeption(DataCalendarNotBeNullException e, HttpServletRequest request) { + log.error(GENERATION_ERROR.getEnumDescription() + e.getClass() + LINE_FEED + e.getMessage() + LINE_FEED + 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(GENERATION_ERROR.getEnumDescription() + e.getClass() + LINE_FEED + e.getMessage() + LINE_FEED + e); + + ApiErrorResponse apiErrorResponse = buildApiErrorResponse( + HttpStatus.NOT_ACCEPTABLE, + e.getMessage(), + request.getRequestURI(), + null + ); + + return new ResponseEntity<>(apiErrorResponse, HttpStatus.NOT_ACCEPTABLE); + } + /** * Обработчик ConstraintViolationException */ diff --git a/src/main/java/com/emobile/springtodo/controllers/TaskController.java b/src/main/java/com/emobile/springtodo/controllers/TaskController.java index 94cc8ca3..035dabc3 100644 --- a/src/main/java/com/emobile/springtodo/controllers/TaskController.java +++ b/src/main/java/com/emobile/springtodo/controllers/TaskController.java @@ -1,7 +1,9 @@ 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; @@ -59,7 +61,7 @@ public class TaskController { @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) { + TaskDto tasksDto) throws DataCalendarNotBeNullException, StatusTaskNotBeNullException { log.info("Создание задачи, POST "); TaskDto localTasksDto = taskService.createTasks(tasksDto); if (localTasksDto != null) { @@ -81,9 +83,10 @@ public ResponseEntity createTask(@Valid @RequestBody @NotNull(message = @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/gen-info/executor/{executorEmail}", consumes = MediaType.ALL_VALUE) + @GetMapping(value = "/task/list/{title}", consumes = MediaType.ALL_VALUE) public ResponseEntity> getTasksByTitle( - @PathVariable("executorEmail") @Parameter(description = "Заголовок задачи") @NotBlank(message = "Заголовок задачи не может быть пустым") String taskTitle, + @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 = "Количество сущностей на странице", @@ -129,14 +132,17 @@ public ResponseEntity editTasks(@Valid @RequestBody @Parameter(descript */ @Operation(summary = "Удалить задачу") @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Задача успешно отредактирована", content = @Content(examples = @ExampleObject(value = "{\nСущность была удалена\n}"))), + @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, produces = MediaType.TEXT_PLAIN_VALUE) + @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); - taskService.deleteTasks(idTasks); - return ResponseEntity.ok(IS_DELETE); + 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 index cc40ad44..14468b9f 100644 --- a/src/main/java/com/emobile/springtodo/dto/TaskDto.java +++ b/src/main/java/com/emobile/springtodo/dto/TaskDto.java @@ -2,7 +2,10 @@ import com.fasterxml.jackson.annotation.JsonFormat; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.*; +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; @@ -35,12 +38,10 @@ public class TaskDto implements Serializable { private String description; @Schema(description = "Статус задачи", example = "true") - @NotNull(message = "Статус задачи не может быть пуст") private Boolean status; @Schema(description = "Дата задачи в календаре", example = "2020-01-01T00:01:10") @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss") - @NotNull(message = "Дата задачи в календаре не может быть пустой") private LocalDateTime dateCalendar; } 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 index 72ebf2c3..696a9ffc 100644 --- a/src/main/java/com/emobile/springtodo/exceptions/DescriptionUserExeption.java +++ b/src/main/java/com/emobile/springtodo/exceptions/DescriptionUserExeption.java @@ -9,7 +9,11 @@ public enum DescriptionUserExeption { GENERATION_ERROR("Возникла ошибка в системе: "), - TASK_NOT_FOUND_BY_ID("Задача по переданному ID не найдена"); + TASK_NOT_FOUND_BY_ID("Задача по переданному ID не найдена"), + + STATUS_NOT_BE_NULL("Статус задачи не может быть пуст"), + + DATE_CALENDAR_NOT_BE_NULL("Дата задачи в календаре не может быть пустой"); private String enumDescription; diff --git a/src/main/java/com/emobile/springtodo/exceptions/EntityNotFoundException.java b/src/main/java/com/emobile/springtodo/exceptions/EntityNotFoundException.java index 43f7dd01..735672db 100644 --- a/src/main/java/com/emobile/springtodo/exceptions/EntityNotFoundException.java +++ b/src/main/java/com/emobile/springtodo/exceptions/EntityNotFoundException.java @@ -1,5 +1,8 @@ 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/mappers/TaskMapper.java b/src/main/java/com/emobile/springtodo/mappers/TaskMapper.java index a7ac6c14..7aa7c367 100644 --- a/src/main/java/com/emobile/springtodo/mappers/TaskMapper.java +++ b/src/main/java/com/emobile/springtodo/mappers/TaskMapper.java @@ -13,7 +13,7 @@ public interface TaskMapper { /** - * Метод, конвертирующий {@link } в {@link } + * Метод, конвертирующий {@link TaskDto} в {@link Task} * * @param tasksDto DTO объекта задачи * @return Сконвертированный объект задачи @@ -21,7 +21,7 @@ public interface TaskMapper { Task convertDtoToTasks(TaskDto tasksDto); /** - * Метод, конвертирующий {@link } в {@link } + * Метод, конвертирующий {@link Task} в {@link TaskDto} * * @param tasks Сущность задачи из базы данных * @return Сконвертированный DTO объекта задачи diff --git a/src/main/java/com/emobile/springtodo/other/ConstantsClass.java b/src/main/java/com/emobile/springtodo/other/ConstantsClass.java index 10901232..ccb82cae 100644 --- a/src/main/java/com/emobile/springtodo/other/ConstantsClass.java +++ b/src/main/java/com/emobile/springtodo/other/ConstantsClass.java @@ -7,12 +7,16 @@ */ 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 = "Сущность была удалена"; 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 = "dateCalendar"; + public static final String DATA_CALENDAR_FIELD = "date_calendar"; public static final String LINE_FEED = "\n"; + public static final String PERCENT = "%"; } diff --git a/src/main/java/com/emobile/springtodo/repositories/JdbcRepository.java b/src/main/java/com/emobile/springtodo/repositories/JdbcRepository.java index e885ad64..f56db361 100644 --- a/src/main/java/com/emobile/springtodo/repositories/JdbcRepository.java +++ b/src/main/java/com/emobile/springtodo/repositories/JdbcRepository.java @@ -1,13 +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 index 0b2bfdc2..9ad3c138 100644 --- a/src/main/java/com/emobile/springtodo/repositories/TaskRepository.java +++ b/src/main/java/com/emobile/springtodo/repositories/TaskRepository.java @@ -1,11 +1,13 @@ package com.emobile.springtodo.repositories; import com.emobile.springtodo.entities.Task; -import org.springframework.stereotype.Repository; 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 index 2c3c0a67..a720e155 100644 --- a/src/main/java/com/emobile/springtodo/repositories/TaskRepositoryImpl.java +++ b/src/main/java/com/emobile/springtodo/repositories/TaskRepositoryImpl.java @@ -1,6 +1,7 @@ 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; @@ -16,6 +17,7 @@ import static com.emobile.springtodo.other.ConstantsClass.*; +@Slf4j @Repository public class TaskRepositoryImpl implements TaskRepository { @@ -26,9 +28,19 @@ 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, dateCalendar) " + + String sql = "INSERT INTO tasks_entity (title, description, status, date_calendar) " + "VALUES (?, ?, ?, ?)"; commonUpdate(sql, entity); return entity; @@ -36,29 +48,30 @@ public Task save(Task entity) { @Override public Task update(Task newEntity) { - String sql = "UPDATE tasks_entity SET title = ?, description = ?, status = ?, dateCalendar = ? WHERE id = ? "; + 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, dateCalendar " + + 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, dateCalendar " + - "FROM tasks_entity WHERE title = ? ORDER BY id LIMIT ? OFFSET ?"; - return Optional.of(jdbcTemplate.query(sql, rowMapper(), title, offset, 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() { diff --git a/src/main/java/com/emobile/springtodo/services/TaskService.java b/src/main/java/com/emobile/springtodo/services/TaskService.java index 9d0ffcd1..a59e6787 100644 --- a/src/main/java/com/emobile/springtodo/services/TaskService.java +++ b/src/main/java/com/emobile/springtodo/services/TaskService.java @@ -1,7 +1,9 @@ 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; @@ -17,9 +19,9 @@ public interface TaskService { * Метод для создания задачи * * @param tasksDto DTO объекта задачи, содержащий данные для создания - * @return Созданный объект-задача в виде {@link } + * @return Созданный объект-задача в виде {@link TaskDto} */ - TaskDto createTasks(TaskDto tasksDto); + TaskDto createTasks(TaskDto tasksDto) throws StatusTaskNotBeNullException, DataCalendarNotBeNullException; /** * Метод для редактирования задачи @@ -43,7 +45,8 @@ public interface TaskService { * Метод для удаления задачи * * @param idTasks ID задачи, которую нужно удалить + * @return true - если задача найдена и удалена успешно */ - void deleteTasks(Long idTasks); + 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 index b1616a93..274b315f 100644 --- a/src/main/java/com/emobile/springtodo/services/TaskServicesImpl.java +++ b/src/main/java/com/emobile/springtodo/services/TaskServicesImpl.java @@ -2,18 +2,27 @@ 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.TASK_NOT_FOUND_BY_ID; +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; @@ -25,15 +34,24 @@ public TaskServicesImpl(TaskMapper taskMapper, TaskRepository taskRepository) { this.taskRepository = taskRepository; } - + @CachePut(cacheNames = "taskServiceCache", key = "#result.id") + @Transactional @Override - public TaskDto createTasks(TaskDto tasksDto) { + 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()); @@ -45,20 +63,41 @@ public Optional changeTasks(TaskDto tasksDto) throws EntityNotFoundExce 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 tasksName, Integer offset, Integer limit) { - Optional> optionalTaskList = taskRepository.findAllByTitle(tasksName, offset, limit); - if (optionalTaskList.isPresent()) { + 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 void deleteTasks(Long idTasks) { - taskRepository.deleteById(idTasks); + 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/db/changelog/V1.0.0-create-table-changelog.xml b/src/main/resources/db/changelog/V1.0.0-create-table-changelog.xml index c3f10524..2d03ef9e 100644 --- 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 @@ -18,7 +18,7 @@ - + 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 index deb37bae..2c133a8a 100644 --- 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 @@ -8,14 +8,14 @@ - + - + From 56232d990b278e19e900270b00dc218a089160d5 Mon Sep 17 00:00:00 2001 From: vsirik Date: Wed, 5 Feb 2025 13:47:04 +0300 Subject: [PATCH 4/9] Junit Tests + docker --- README.md | 2 +- docker-compose.yaml | 19 ++- pom.xml | 8 ++ .../springtodo/other/ConstantsClass.java | 2 +- .../springtodo/services/TaskServicesImpl.java | 4 +- src/main/resources/application.properties | 6 +- .../controllers/TaskControllerTest.java | 135 ++++++++++++++++++ .../repositories/TaskRepositoryTest.java | 78 ++++++++++ .../springtodo/services/TaskServicesTest.java | 112 +++++++++++++++ 9 files changed, 360 insertions(+), 6 deletions(-) create mode 100644 src/test/java/com/emobile/springtodo/controllers/TaskControllerTest.java create mode 100644 src/test/java/com/emobile/springtodo/repositories/TaskRepositoryTest.java create mode 100644 src/test/java/com/emobile/springtodo/services/TaskServicesTest.java diff --git a/README.md b/README.md index 1237b0c2..507b2a95 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Swagger находится по адресу - http://localhost:8081/swagger-ui/index.html# PgAdmin находится по адресу - http://localhost:5050/, логин admin@admin.com, пароль root -Для того, чтобы поднять проект в докере(пгадмин, приложение, БД), необходимо установить переменные среды на ПК, в +Для того, чтобы поднять проект в докере(редис, пгадмин, приложение, БД), необходимо установить переменные среды на ПК, в *Переменных среды для пользователя <имя пользователя>* * Имя переменной: `BASEPROJECT_DATABASE_USERNAME` diff --git a/docker-compose.yaml b/docker-compose.yaml index 7099b20f..c9f4e88b 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -41,7 +41,24 @@ services: 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: \ No newline at end of file + postgres_data: + redis_data: \ No newline at end of file diff --git a/pom.xml b/pom.xml index af2372ff..f5a69767 100644 --- a/pom.xml +++ b/pom.xml @@ -67,6 +67,14 @@ runtime + + + org.springframework.boot + spring-boot-starter-data-redis + 3.4.2 + + + diff --git a/src/main/java/com/emobile/springtodo/other/ConstantsClass.java b/src/main/java/com/emobile/springtodo/other/ConstantsClass.java index ccb82cae..eef001b2 100644 --- a/src/main/java/com/emobile/springtodo/other/ConstantsClass.java +++ b/src/main/java/com/emobile/springtodo/other/ConstantsClass.java @@ -10,7 +10,7 @@ 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 = "Сущность была удалена"; + 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"; diff --git a/src/main/java/com/emobile/springtodo/services/TaskServicesImpl.java b/src/main/java/com/emobile/springtodo/services/TaskServicesImpl.java index 274b315f..288f31b6 100644 --- a/src/main/java/com/emobile/springtodo/services/TaskServicesImpl.java +++ b/src/main/java/com/emobile/springtodo/services/TaskServicesImpl.java @@ -73,10 +73,10 @@ public Optional changeTasks(TaskDto tasksDto) throws EntityNotFoundExce public Optional> getTasksByTitle(String taskTitle, Integer offset, Integer limit) { StringBuffer localStringBuffer = new StringBuffer(taskTitle); char[] chars = taskTitle.toCharArray(); - if (chars[ZERO] == PERCENT_CHAR_PRIMITIVE) { + if (chars[ZERO] != PERCENT_CHAR_PRIMITIVE) { localStringBuffer.insert(ZERO, PERCENT); } - if (chars[chars.length - ONE] == PERCENT_CHAR_PRIMITIVE) { + if (chars[chars.length - ONE] != PERCENT_CHAR_PRIMITIVE) { localStringBuffer.append(PERCENT); } Optional> optionalTaskList = taskRepository.findAllByTitle(localStringBuffer.toString(), offset, limit); diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 855d4d3c..d4df0a57 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -6,4 +6,8 @@ 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 \ No newline at end of file +spring.liquibase.enabled=true +spring.cache.type=redis +spring.redis.host=localhost +spring.redis.port=6379 +spring.cache.cache-names=taskServiceCache \ 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..25225c6a --- /dev/null +++ b/src/test/java/com/emobile/springtodo/controllers/TaskControllerTest.java @@ -0,0 +1,135 @@ +// TaskControllerIntegrationTest.java +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; + + // Тесты для POST /task/create + + @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()); + } + + // Тесты для GET /task/list/{title} + @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()); + } + + // Тесты для PUT /task/update-tasks + @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()); + } + + // Тесты для DELETE /task/delete/{id} + @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()); + } +} From cb0175afbba38b2e0722072d5b9322e68c72d713 Mon Sep 17 00:00:00 2001 From: vsirik Date: Wed, 5 Feb 2025 15:15:45 +0300 Subject: [PATCH 5/9] fix --- .../java/com/emobile/springtodo/config/OpenApiConfig.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/emobile/springtodo/config/OpenApiConfig.java b/src/main/java/com/emobile/springtodo/config/OpenApiConfig.java index e2682111..ce0805da 100644 --- a/src/main/java/com/emobile/springtodo/config/OpenApiConfig.java +++ b/src/main/java/com/emobile/springtodo/config/OpenApiConfig.java @@ -5,8 +5,8 @@ import io.swagger.v3.oas.annotations.info.Info; @OpenAPIDefinition(info = @Info( - title = "TaskDto Management System", - description = "TO-DO List", + title = "TO-DO List", + description = "TO-DO List System", version = "1.0.0", contact = @Contact( name = "Sirik Vadim" From 629b17e19ac9b18c45d5e885b1040a0ff0b80f5a Mon Sep 17 00:00:00 2001 From: vsirik Date: Thu, 6 Feb 2025 20:34:32 +0300 Subject: [PATCH 6/9] pojo -> record class --- .../com/emobile/springtodo/exceptions/Violation.java | 12 +----------- .../springtodo/controllers/TaskControllerTest.java | 6 ------ 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/src/main/java/com/emobile/springtodo/exceptions/Violation.java b/src/main/java/com/emobile/springtodo/exceptions/Violation.java index 37b4766e..1d0eba84 100644 --- a/src/main/java/com/emobile/springtodo/exceptions/Violation.java +++ b/src/main/java/com/emobile/springtodo/exceptions/Violation.java @@ -1,13 +1,3 @@ package com.emobile.springtodo.exceptions; -import lombok.Getter; -import lombok.RequiredArgsConstructor; - -@Getter -@RequiredArgsConstructor -public class Violation { - - private final String fieldName; - private final String message; - -} +public record Violation(String fieldName, String message) {} diff --git a/src/test/java/com/emobile/springtodo/controllers/TaskControllerTest.java b/src/test/java/com/emobile/springtodo/controllers/TaskControllerTest.java index 25225c6a..fbdb555a 100644 --- a/src/test/java/com/emobile/springtodo/controllers/TaskControllerTest.java +++ b/src/test/java/com/emobile/springtodo/controllers/TaskControllerTest.java @@ -1,4 +1,3 @@ -// TaskControllerIntegrationTest.java package com.emobile.springtodo.controllers; import com.emobile.springtodo.dto.TaskDto; @@ -35,8 +34,6 @@ class TaskControllerTest { @Autowired private ObjectMapper objectMapper; - // Тесты для POST /task/create - @DisplayName("Проверка POST /task/create, при валидном запросе") @Test void createTask_ValidRequest_ReturnsCreatedTask() throws Exception { @@ -63,7 +60,6 @@ void createTask_InvalidRequest_ReturnsBadRequest() throws Exception { .andExpect(status().isBadRequest()); } - // Тесты для GET /task/list/{title} @DisplayName("Проверка GET /task/list/{title}, при валидном запросе") @Test void getTasksByTitle_ExistingTitle_ReturnsTasks() throws Exception { @@ -85,7 +81,6 @@ void getTasksByTitle_NonExistingTitle_ReturnsBadRequest() throws Exception { .andExpect(status().isBadRequest()); } - // Тесты для PUT /task/update-tasks @DisplayName("Проверка GET /task/list/{title}, при валидном запросе") @Test void updateTask_ValidRequest_ReturnsUpdatedTask() throws Exception { @@ -113,7 +108,6 @@ void updateTask_NonExistingTask_ReturnsBadRequest() throws Exception { .andExpect(status().isBadRequest()); } - // Тесты для DELETE /task/delete/{id} @DisplayName("Проверка DELETE /task/delete/{id}, при валидном запросе") @Test void deleteTask_ExistingId_ReturnsSuccess() throws Exception { From 5ac332aa8b6c075e0251cb4966df5b8dd5f19d61 Mon Sep 17 00:00:00 2001 From: vsirik Date: Thu, 6 Feb 2025 20:36:17 +0300 Subject: [PATCH 7/9] spring.data.redis --- src/main/resources/application.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index d4df0a57..4ba605bd 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -8,6 +8,6 @@ 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.redis.host=localhost -spring.redis.port=6379 +spring.data.redis.host=localhost +spring.data.redis.port=6379 spring.cache.cache-names=taskServiceCache \ No newline at end of file From 2caecaf5ad83a86bc982b0c46a50d54b16a8836d Mon Sep 17 00:00:00 2001 From: vsirik Date: Thu, 6 Feb 2025 23:14:58 +0300 Subject: [PATCH 8/9] change return GET method --- .../emobile/springtodo/controllers/TaskController.java | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/emobile/springtodo/controllers/TaskController.java b/src/main/java/com/emobile/springtodo/controllers/TaskController.java index 035dabc3..b712e8d4 100644 --- a/src/main/java/com/emobile/springtodo/controllers/TaskController.java +++ b/src/main/java/com/emobile/springtodo/controllers/TaskController.java @@ -95,10 +95,7 @@ public ResponseEntity> getTasksByTitle( log.info("Получение задачи по исполнителю, метод GET " + taskTitle); Optional> optionalExecutorTasksDtoList = taskService .getTasksByTitle(taskTitle, offset, limit); - if (optionalExecutorTasksDtoList.isPresent()) { - return ResponseEntity.ok(optionalExecutorTasksDtoList.get()); - } - return ResponseEntity.badRequest().build(); + return optionalExecutorTasksDtoList.map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.badRequest().build()); } /** @@ -118,10 +115,7 @@ public ResponseEntity editTasks(@Valid @RequestBody @Parameter(descript @NotNull(message = "TasksDto не может быть null") TaskDto tasksDto) throws EntityNotFoundException { Optional newTasksDto = taskService .changeTasks(tasksDto); - if (newTasksDto.isPresent()) { - return ResponseEntity.ok(newTasksDto.get()); - } - return ResponseEntity.badRequest().build(); + return newTasksDto.map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.badRequest().build()); } /** From ecace90c6869d40d398d67eaac79433501d21dd0 Mon Sep 17 00:00:00 2001 From: vsirik Date: Thu, 6 Feb 2025 23:24:34 +0300 Subject: [PATCH 9/9] logger --- .../HandlerExceptionController.java | 29 +++++++------------ .../springtodo/other/ConstantsClass.java | 1 + 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/emobile/springtodo/controllers/HandlerExceptionController.java b/src/main/java/com/emobile/springtodo/controllers/HandlerExceptionController.java index 8ae09886..8b50eebf 100644 --- a/src/main/java/com/emobile/springtodo/controllers/HandlerExceptionController.java +++ b/src/main/java/com/emobile/springtodo/controllers/HandlerExceptionController.java @@ -21,7 +21,7 @@ import java.util.stream.Collectors; import static com.emobile.springtodo.exceptions.DescriptionUserExeption.GENERATION_ERROR; -import static com.emobile.springtodo.other.ConstantsClass.LINE_FEED; +import static com.emobile.springtodo.other.ConstantsClass.LOGGER_SYNTAX; /** *
@@ -37,7 +37,7 @@ public class HandlerExceptionController {
      */
     @ExceptionHandler(EntityNotFoundException.class)
     protected ResponseEntity handleExecutorNotFoundExeption(EntityNotFoundException e, HttpServletRequest request) {
-        log.error(GENERATION_ERROR.getEnumDescription() + e.getClass() + LINE_FEED + e.getMessage() + LINE_FEED + e);
+        log.error(LOGGER_SYNTAX, GENERATION_ERROR.getEnumDescription(), e.getClass(), e.getMessage(), e);
 
         ApiErrorResponse apiErrorResponse = buildApiErrorResponse(
                 HttpStatus.NOT_FOUND,
@@ -51,7 +51,7 @@ protected ResponseEntity handleExecutorNotFoundExeption(Entity
 
     @ExceptionHandler(DataCalendarNotBeNullException.class)
     protected ResponseEntity handleExecutorNotFoundExeption(DataCalendarNotBeNullException e, HttpServletRequest request) {
-        log.error(GENERATION_ERROR.getEnumDescription() + e.getClass() + LINE_FEED + e.getMessage() + LINE_FEED + e);
+        log.error(LOGGER_SYNTAX, GENERATION_ERROR.getEnumDescription(), e.getClass(), e.getMessage(), e);
 
         ApiErrorResponse apiErrorResponse = buildApiErrorResponse(
                 HttpStatus.NOT_ACCEPTABLE,
@@ -65,7 +65,7 @@ protected ResponseEntity handleExecutorNotFoundExeption(DataCa
 
     @ExceptionHandler(StatusTaskNotBeNullException.class)
     protected ResponseEntity handleExecutorNotFoundExeption(StatusTaskNotBeNullException e, HttpServletRequest request) {
-        log.error(GENERATION_ERROR.getEnumDescription() + e.getClass() + LINE_FEED + e.getMessage() + LINE_FEED + e);
+        log.error(LOGGER_SYNTAX, GENERATION_ERROR.getEnumDescription(), e.getClass(), e.getMessage(), e);
 
         ApiErrorResponse apiErrorResponse = buildApiErrorResponse(
                 HttpStatus.NOT_ACCEPTABLE,
@@ -83,8 +83,7 @@ protected ResponseEntity handleExecutorNotFoundExeption(Status
     @ExceptionHandler(ConstraintViolationException.class)
     protected ResponseEntity handleConstraintViolationException(ConstraintViolationException e,
                                                                                   HttpServletRequest request) {
-        log.error(GENERATION_ERROR.getEnumDescription() + e.getClass() + LINE_FEED + e.getMessage() + LINE_FEED +
-                e);
+        log.error(LOGGER_SYNTAX, GENERATION_ERROR.getEnumDescription(), e.getClass(), e.getMessage(), e);
 
         Set> violationsSet = e.getConstraintViolations();
         List violations = (violationsSet != null)
@@ -111,8 +110,7 @@ protected ResponseEntity handleConstraintViolationException(Co
      */
     @ExceptionHandler(MethodArgumentNotValidException.class)
     protected ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException e, HttpServletRequest request) {
-        log.error(GENERATION_ERROR.getEnumDescription() + e.getClass() + LINE_FEED + e.getMessage() + LINE_FEED +
-                e);
+        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()))
@@ -133,8 +131,7 @@ protected ResponseEntity handleMethodArgumentNotValidException
      */
     @ExceptionHandler(HttpMessageNotReadableException.class)
     protected ResponseEntity handleHttpMessageNotReadableException(HttpMessageNotReadableException e, HttpServletRequest request) {
-        log.error(GENERATION_ERROR.getEnumDescription() + e.getClass() + LINE_FEED + e.getMessage() + LINE_FEED +
-                e);
+        log.error(LOGGER_SYNTAX, GENERATION_ERROR.getEnumDescription(), e.getClass(), e.getMessage(), e);
 
         ApiErrorResponse apiErrorResponse = buildApiErrorResponse(
                 HttpStatus.BAD_REQUEST,
@@ -151,8 +148,7 @@ protected ResponseEntity handleHttpMessageNotReadableException
      */
     @ExceptionHandler(IllegalStateException.class)
     protected ResponseEntity handleIllegalStateException(IllegalStateException e, HttpServletRequest request) {
-        log.error(GENERATION_ERROR.getEnumDescription() + e.getClass() + LINE_FEED + e.getMessage() + LINE_FEED +
-                e);
+        log.error(LOGGER_SYNTAX, GENERATION_ERROR.getEnumDescription(), e.getClass(), e.getMessage(), e);
 
         ApiErrorResponse apiErrorResponse = buildApiErrorResponse(
                 HttpStatus.INTERNAL_SERVER_ERROR,
@@ -170,8 +166,7 @@ protected ResponseEntity handleIllegalStateException(IllegalSt
     @ExceptionHandler(DataIntegrityViolationException.class)
     protected ResponseEntity handleDataIntegrityViolationException(DataIntegrityViolationException e,
                                                                                      HttpServletRequest request) {
-        log.error(GENERATION_ERROR.getEnumDescription() + e.getClass() + LINE_FEED + e.getMessage() + LINE_FEED +
-                e);
+        log.error(LOGGER_SYNTAX, GENERATION_ERROR.getEnumDescription(), e.getClass(), e.getMessage(), e);
 
         ApiErrorResponse apiErrorResponse = buildApiErrorResponse(
                 HttpStatus.INTERNAL_SERVER_ERROR,
@@ -188,8 +183,7 @@ protected ResponseEntity handleDataIntegrityViolationException
      */
     @ExceptionHandler(Exception.class)
     protected ResponseEntity handleAllExceptions(Exception e, HttpServletRequest request) {
-        log.error("Возникла непредвиденная ошибка " + e.getClass() + LINE_FEED + e.getMessage() + LINE_FEED +
-                e);
+        log.error(LOGGER_SYNTAX, GENERATION_ERROR.getEnumDescription(), e.getClass(), e.getMessage(), e);
 
         ApiErrorResponse apiErrorResponse = buildApiErrorResponse(
                 HttpStatus.INTERNAL_SERVER_ERROR,
@@ -203,8 +197,7 @@ protected ResponseEntity handleAllExceptions(Exception e, Http
 
     @ExceptionHandler(RuntimeException.class)
     protected ResponseEntity handleAllRunTimeException(RuntimeException e, HttpServletRequest request) {
-        log.error("Возникла непредвиденная ошибка " + e.getClass() + LINE_FEED + e.getMessage() + LINE_FEED +
-                e);
+        log.error(LOGGER_SYNTAX, GENERATION_ERROR.getEnumDescription(), e.getClass(), e.getMessage(), e);
 
         ApiErrorResponse apiErrorResponse = buildApiErrorResponse(
                 HttpStatus.INTERNAL_SERVER_ERROR,
diff --git a/src/main/java/com/emobile/springtodo/other/ConstantsClass.java b/src/main/java/com/emobile/springtodo/other/ConstantsClass.java
index eef001b2..c77d18e4 100644
--- a/src/main/java/com/emobile/springtodo/other/ConstantsClass.java
+++ b/src/main/java/com/emobile/springtodo/other/ConstantsClass.java
@@ -18,5 +18,6 @@ public class ConstantsClass {
     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{}";
 
 }