From 5e90c6383feb84f0a399f9e40365ca312b1b0386 Mon Sep 17 00:00:00 2001 From: sasha Date: Tue, 10 Feb 2026 16:57:53 +0300 Subject: [PATCH 1/7] lab1_REST --- 351004/Brazhalovich/Lab_1/pom.xml | 102 ++++++++++++++++++ .../example/newsapi/NewsApiApplication.java | 11 ++ .../org/example/newsapi/config/WebConfig.java | 0 .../newsapi/controller/CommentController.java | 50 +++++++++ .../newsapi/controller/MarkerController.java | 45 ++++++++ .../newsapi/controller/NewsController.java | 45 ++++++++ .../newsapi/controller/UserController.java | 45 ++++++++ .../newsapi/dto/request/CommentRequestTo.java | 14 +++ .../newsapi/dto/request/MarkerRequestTo.java | 10 ++ .../newsapi/dto/request/NewsRequestTo.java | 20 ++++ .../newsapi/dto/request/UserRequestTo.java | 19 ++++ .../dto/response/CommentResponseTo.java | 10 ++ .../dto/response/MarkerResponseTo.java | 9 ++ .../newsapi/dto/response/NewsResponseTo.java | 16 +++ .../newsapi/dto/response/UserResponseTo.java | 11 ++ .../example/newsapi/entity/BaseEntity.java | 9 ++ .../org/example/newsapi/entity/Comment.java | 11 ++ .../org/example/newsapi/entity/Marker.java | 10 ++ .../java/org/example/newsapi/entity/News.java | 19 ++++ .../java/org/example/newsapi/entity/User.java | 13 +++ .../newsapi/exception/ErrorResponse.java | 10 ++ .../exception/GlobalExceptionHandler.java | 31 ++++++ .../newsapi/exception/NotFoundException.java | 7 ++ .../example/newsapi/mapper/CommentMapper.java | 20 ++++ .../example/newsapi/mapper/MarkerMapper.java | 20 ++++ .../example/newsapi/mapper/NewsMapper.java | 23 ++++ .../example/newsapi/mapper/UserMapper.java | 17 +++ .../newsapi/repository/CrudRepository.java | 12 +++ .../repository/InMemoryRepository.java | 46 ++++++++ .../repository/impl/CommentRepository.java | 9 ++ .../repository/impl/MarkerRepository.java | 9 ++ .../repository/impl/NewsRepository.java | 9 ++ .../repository/impl/UserRepository.java | 9 ++ .../newsapi/service/CommentService.java | 59 ++++++++++ .../newsapi/service/MarkerService.java | 54 ++++++++++ .../example/newsapi/service/NewsService.java | 61 +++++++++++ .../example/newsapi/service/UserService.java | 52 +++++++++ .../src/main/resources/application.properties | 1 + 38 files changed, 918 insertions(+) create mode 100644 351004/Brazhalovich/Lab_1/pom.xml create mode 100644 351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/NewsApiApplication.java create mode 100644 351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/config/WebConfig.java create mode 100644 351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/controller/CommentController.java create mode 100644 351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/controller/MarkerController.java create mode 100644 351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/controller/NewsController.java create mode 100644 351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/controller/UserController.java create mode 100644 351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/request/CommentRequestTo.java create mode 100644 351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/request/MarkerRequestTo.java create mode 100644 351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/request/NewsRequestTo.java create mode 100644 351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/request/UserRequestTo.java create mode 100644 351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/response/CommentResponseTo.java create mode 100644 351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/response/MarkerResponseTo.java create mode 100644 351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/response/NewsResponseTo.java create mode 100644 351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/response/UserResponseTo.java create mode 100644 351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/entity/BaseEntity.java create mode 100644 351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/entity/Comment.java create mode 100644 351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/entity/Marker.java create mode 100644 351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/entity/News.java create mode 100644 351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/entity/User.java create mode 100644 351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/exception/ErrorResponse.java create mode 100644 351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/exception/GlobalExceptionHandler.java create mode 100644 351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/exception/NotFoundException.java create mode 100644 351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/mapper/CommentMapper.java create mode 100644 351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/mapper/MarkerMapper.java create mode 100644 351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/mapper/NewsMapper.java create mode 100644 351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/mapper/UserMapper.java create mode 100644 351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/CrudRepository.java create mode 100644 351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/InMemoryRepository.java create mode 100644 351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/impl/CommentRepository.java create mode 100644 351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/impl/MarkerRepository.java create mode 100644 351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/impl/NewsRepository.java create mode 100644 351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/impl/UserRepository.java create mode 100644 351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/service/CommentService.java create mode 100644 351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/service/MarkerService.java create mode 100644 351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/service/NewsService.java create mode 100644 351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/service/UserService.java create mode 100644 351004/Brazhalovich/Lab_1/src/main/resources/application.properties diff --git a/351004/Brazhalovich/Lab_1/pom.xml b/351004/Brazhalovich/Lab_1/pom.xml new file mode 100644 index 000000000..ecb26433e --- /dev/null +++ b/351004/Brazhalovich/Lab_1/pom.xml @@ -0,0 +1,102 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.2.2 + + + + org.example + untitled + 1.0-SNAPSHOT + + + 21 + 1.5.5.Final + 1.18.30 + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.springframework.boot + spring-boot-starter-validation + + + + + org.projectlombok + lombok + ${lombok.version} + true + + + + + org.mapstruct + mapstruct + ${org.mapstruct.version} + + + + + org.springframework.boot + spring-boot-starter-test + test + + + io.rest-assured + rest-assured + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + ${java.version} + ${java.version} + + + org.projectlombok + lombok + ${lombok.version} + + + org.mapstruct + mapstruct-processor + ${org.mapstruct.version} + + + + + + + \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/NewsApiApplication.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/NewsApiApplication.java new file mode 100644 index 000000000..97f1fa270 --- /dev/null +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/NewsApiApplication.java @@ -0,0 +1,11 @@ +package org.example.newsapi; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class NewsApiApplication { + public static void main(String[] args) { + SpringApplication.run(NewsApiApplication.class, args); + } +} \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/config/WebConfig.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/config/WebConfig.java new file mode 100644 index 000000000..e69de29bb diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/controller/CommentController.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/controller/CommentController.java new file mode 100644 index 000000000..9e8e7a469 --- /dev/null +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/controller/CommentController.java @@ -0,0 +1,50 @@ +package org.example.newsapi.controller; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.example.newsapi.dto.request.CommentRequestTo; +import org.example.newsapi.dto.response.CommentResponseTo; +import org.example.newsapi.service.CommentService; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/v1.0/comments") +@RequiredArgsConstructor +public class CommentController { + private final CommentService commentService; + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public CommentResponseTo create(@RequestBody @Valid CommentRequestTo request) { + return commentService.create(request); + } + + @GetMapping + public List getAll() { + return commentService.findAll(); + } + + @GetMapping("/{id}") + public CommentResponseTo getById(@PathVariable Long id) { + return commentService.findById(id); + } + + @PutMapping("/{id}") + public CommentResponseTo update(@PathVariable Long id, @RequestBody @Valid CommentRequestTo request) { + return commentService.update(id, request); + } + + @DeleteMapping("/{id}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void delete(@PathVariable Long id) { + commentService.delete(id); + } + @GetMapping("/by-news/{newsId}") + public List getByNewsId(@PathVariable Long newsId) { + return commentService.findByNewsId(newsId); + } + +} \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/controller/MarkerController.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/controller/MarkerController.java new file mode 100644 index 000000000..c24dc4bcb --- /dev/null +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/controller/MarkerController.java @@ -0,0 +1,45 @@ +package org.example.newsapi.controller; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.example.newsapi.dto.request.MarkerRequestTo; +import org.example.newsapi.dto.response.MarkerResponseTo; +import org.example.newsapi.service.MarkerService; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/v1.0/markers") +@RequiredArgsConstructor +public class MarkerController { + private final MarkerService markerService; + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public MarkerResponseTo create(@RequestBody @Valid MarkerRequestTo request) { + return markerService.create(request); + } + + @GetMapping + public List getAll() { + return markerService.findAll(); + } + + @GetMapping("/{id}") + public MarkerResponseTo getById(@PathVariable Long id) { + return markerService.findById(id); + } + + @PutMapping("/{id}") + public MarkerResponseTo update(@PathVariable Long id, @RequestBody @Valid MarkerRequestTo request) { + return markerService.update(id, request); + } + + @DeleteMapping("/{id}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void delete(@PathVariable Long id) { + markerService.delete(id); + } +} \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/controller/NewsController.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/controller/NewsController.java new file mode 100644 index 000000000..df2b53c88 --- /dev/null +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/controller/NewsController.java @@ -0,0 +1,45 @@ +package org.example.newsapi.controller; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.example.newsapi.dto.request.NewsRequestTo; +import org.example.newsapi.dto.response.NewsResponseTo; +import org.example.newsapi.service.NewsService; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/v1.0/news") +@RequiredArgsConstructor +public class NewsController { + private final NewsService newsService; + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public NewsResponseTo create(@RequestBody @Valid NewsRequestTo request) { + return newsService.create(request); + } + + @GetMapping + public List getAll() { + return newsService.findAll(); + } + + @GetMapping("/{id}") + public NewsResponseTo getById(@PathVariable Long id) { + return newsService.findById(id); + } + + @PutMapping("/{id}") + public NewsResponseTo update(@PathVariable Long id, @RequestBody @Valid NewsRequestTo request) { + return newsService.update(id, request); + } + + @DeleteMapping("/{id}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void delete(@PathVariable Long id) { + newsService.delete(id); + } +} \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/controller/UserController.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/controller/UserController.java new file mode 100644 index 000000000..fa4a33fd6 --- /dev/null +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/controller/UserController.java @@ -0,0 +1,45 @@ +package org.example.newsapi.controller; + +import org.example.newsapi.dto.request.UserRequestTo; +import org.example.newsapi.dto.response.UserResponseTo; +import org.example.newsapi.service.UserService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/v1.0/users") +@RequiredArgsConstructor +public class UserController { + private final UserService userService; + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public UserResponseTo create(@RequestBody @Valid UserRequestTo request) { + return userService.create(request); + } + + @GetMapping + public List getAll() { + return userService.findAll(); + } + + @GetMapping("/{id}") + public UserResponseTo getById(@PathVariable Long id) { + return userService.findById(id); + } + + @PutMapping("/{id}") + public UserResponseTo update(@PathVariable Long id, @RequestBody @Valid UserRequestTo request) { + return userService.update(id, request); + } + + @DeleteMapping("/{id}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void delete(@PathVariable Long id) { + userService.delete(id); + } +} \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/request/CommentRequestTo.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/request/CommentRequestTo.java new file mode 100644 index 000000000..630ea739f --- /dev/null +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/request/CommentRequestTo.java @@ -0,0 +1,14 @@ +package org.example.newsapi.dto.request; + +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Data +public class CommentRequestTo { + @NotNull + private Long newsId; + + @Size(min = 2, max = 2048) + private String content; +} \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/request/MarkerRequestTo.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/request/MarkerRequestTo.java new file mode 100644 index 000000000..fe0cd99c3 --- /dev/null +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/request/MarkerRequestTo.java @@ -0,0 +1,10 @@ +package org.example.newsapi.dto.request; + +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Data +public class MarkerRequestTo { + @Size(min = 2, max = 32) + private String name; +} \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/request/NewsRequestTo.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/request/NewsRequestTo.java new file mode 100644 index 000000000..f117a445f --- /dev/null +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/request/NewsRequestTo.java @@ -0,0 +1,20 @@ +package org.example.newsapi.dto.request; + +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; +import java.util.Set; + +@Data +public class NewsRequestTo { + @NotNull + private Long userId; + + @Size(min = 2, max = 64) + private String title; + + @Size(min = 4, max = 2048) + private String content; + + private Set markerIds; +} \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/request/UserRequestTo.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/request/UserRequestTo.java new file mode 100644 index 000000000..91e0f048a --- /dev/null +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/request/UserRequestTo.java @@ -0,0 +1,19 @@ +package org.example.newsapi.dto.request; + +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Data +public class UserRequestTo { + @Size(min = 2, max = 64) + private String login; + + @Size(min = 8, max = 128) + private String password; + + @Size(min = 2, max = 64) + private String firstname; + + @Size(min = 2, max = 64) + private String lastname; +} \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/response/CommentResponseTo.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/response/CommentResponseTo.java new file mode 100644 index 000000000..b7dac9a85 --- /dev/null +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/response/CommentResponseTo.java @@ -0,0 +1,10 @@ +package org.example.newsapi.dto.response; + +import lombok.Data; + +@Data +public class CommentResponseTo { + private Long id; + private Long newsId; + private String content; +} \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/response/MarkerResponseTo.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/response/MarkerResponseTo.java new file mode 100644 index 000000000..d4a3708fc --- /dev/null +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/response/MarkerResponseTo.java @@ -0,0 +1,9 @@ +package org.example.newsapi.dto.response; + +import lombok.Data; + +@Data +public class MarkerResponseTo { + private Long id; + private String name; +} \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/response/NewsResponseTo.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/response/NewsResponseTo.java new file mode 100644 index 000000000..2b183223b --- /dev/null +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/response/NewsResponseTo.java @@ -0,0 +1,16 @@ +package org.example.newsapi.dto.response; + +import lombok.Data; +import java.time.LocalDateTime; +import java.util.Set; + +@Data +public class NewsResponseTo { + private Long id; + private Long userId; + private String title; + private String content; + private LocalDateTime created; + private LocalDateTime modified; + private Set markerIds; // Для упрощения пока возвращаем ID +} \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/response/UserResponseTo.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/response/UserResponseTo.java new file mode 100644 index 000000000..5c8e1d964 --- /dev/null +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/response/UserResponseTo.java @@ -0,0 +1,11 @@ +package org.example.newsapi.dto.response; + +import lombok.Data; + +@Data +public class UserResponseTo { + private Long id; + private String login; + private String firstname; + private String lastname; +} \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/entity/BaseEntity.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/entity/BaseEntity.java new file mode 100644 index 000000000..d6222bb59 --- /dev/null +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/entity/BaseEntity.java @@ -0,0 +1,9 @@ +package org.example.newsapi.entity; + +import lombok.Data; +import java.io.Serializable; + +@Data +public abstract class BaseEntity implements Serializable { + private Long id; +} \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/entity/Comment.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/entity/Comment.java new file mode 100644 index 000000000..923a6525e --- /dev/null +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/entity/Comment.java @@ -0,0 +1,11 @@ +package org.example.newsapi.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class Comment extends BaseEntity { + private Long newsId; // FK + private String content; +} \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/entity/Marker.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/entity/Marker.java new file mode 100644 index 000000000..d98c0ff3a --- /dev/null +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/entity/Marker.java @@ -0,0 +1,10 @@ +package org.example.newsapi.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class Marker extends BaseEntity { + private String name; +} \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/entity/News.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/entity/News.java new file mode 100644 index 000000000..5b3d5f547 --- /dev/null +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/entity/News.java @@ -0,0 +1,19 @@ +package org.example.newsapi.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import java.time.LocalDateTime; +import java.util.HashSet; +import java.util.Set; + +@Data +@EqualsAndHashCode(callSuper = true) +public class News extends BaseEntity { + private Long userId; // FK + private String title; + private String content; + private LocalDateTime created; + private LocalDateTime modified; + // Many-to-Many имитация через хранение ID маркеров + private Set markerIds = new HashSet<>(); +} \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/entity/User.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/entity/User.java new file mode 100644 index 000000000..08ce0912f --- /dev/null +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/entity/User.java @@ -0,0 +1,13 @@ +package org.example.newsapi.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class User extends BaseEntity { + private String login; + private String password; + private String firstname; + private String lastname; +} \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/exception/ErrorResponse.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/exception/ErrorResponse.java new file mode 100644 index 000000000..48c0bc09c --- /dev/null +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/exception/ErrorResponse.java @@ -0,0 +1,10 @@ +package org.example.newsapi.exception; +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class ErrorResponse { + private String errorMessage; + private int errorCode; // 5 digits +} \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/exception/GlobalExceptionHandler.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/exception/GlobalExceptionHandler.java new file mode 100644 index 000000000..de469e2cf --- /dev/null +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/exception/GlobalExceptionHandler.java @@ -0,0 +1,31 @@ +package org.example.newsapi.exception; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(NotFoundException.class) + public ResponseEntity handleNotFound(NotFoundException e) { + // 404 + 01 (arbitrary suffix) + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(new ErrorResponse(e.getMessage(), 40401)); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleValidation(MethodArgumentNotValidException e) { + String message = e.getBindingResult().getAllErrors().get(0).getDefaultMessage(); + // 400 + 01 + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(new ErrorResponse("Validation error: " + message, 40001)); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleAll(Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(new ErrorResponse(e.getMessage(), 50000)); + } +} \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/exception/NotFoundException.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/exception/NotFoundException.java new file mode 100644 index 000000000..a93d2d71c --- /dev/null +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/exception/NotFoundException.java @@ -0,0 +1,7 @@ +package org.example.newsapi.exception; + +public class NotFoundException extends RuntimeException { + public NotFoundException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/mapper/CommentMapper.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/mapper/CommentMapper.java new file mode 100644 index 000000000..bd5ac4aa2 --- /dev/null +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/mapper/CommentMapper.java @@ -0,0 +1,20 @@ +package org.example.newsapi.mapper; + +import org.example.newsapi.dto.request.CommentRequestTo; +import org.example.newsapi.dto.request.MarkerRequestTo; +import org.example.newsapi.dto.response.CommentResponseTo; +import org.example.newsapi.dto.response.MarkerResponseTo; +import org.example.newsapi.entity.Comment; +import org.example.newsapi.entity.Marker; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; + +@Mapper(componentModel = "spring") +public interface CommentMapper { + Comment toEntity(CommentRequestTo request); + CommentResponseTo toDto(Comment comment); + + @Mapping(target = "id", ignore = true) + void updateEntityFromDto(CommentRequestTo request, @MappingTarget Comment comment); +} \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/mapper/MarkerMapper.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/mapper/MarkerMapper.java new file mode 100644 index 000000000..6e4d2ebf7 --- /dev/null +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/mapper/MarkerMapper.java @@ -0,0 +1,20 @@ +package org.example.newsapi.mapper; + +import org.example.newsapi.dto.request.MarkerRequestTo; +import org.example.newsapi.dto.request.UserRequestTo; +import org.example.newsapi.dto.response.MarkerResponseTo; +import org.example.newsapi.dto.response.UserResponseTo; +import org.example.newsapi.entity.Marker; +import org.example.newsapi.entity.User; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; + +@Mapper(componentModel = "spring") +public interface MarkerMapper { + Marker toEntity(MarkerRequestTo request); + MarkerResponseTo toDto(Marker marker); + + @Mapping(target = "id", ignore = true) + void updateEntityFromDto(MarkerRequestTo request, @MappingTarget Marker marker); +} \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/mapper/NewsMapper.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/mapper/NewsMapper.java new file mode 100644 index 000000000..b66a9d1cb --- /dev/null +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/mapper/NewsMapper.java @@ -0,0 +1,23 @@ +package org.example.newsapi.mapper; + +import org.example.newsapi.dto.request.NewsRequestTo; +import org.example.newsapi.dto.response.NewsResponseTo; +import org.example.newsapi.entity.News; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; + +@Mapper(componentModel = "spring") +public interface NewsMapper { + @Mapping(target = "id", ignore = true) + @Mapping(target = "created", ignore = true) + @Mapping(target = "modified", ignore = true) + News toEntity(NewsRequestTo request); + + NewsResponseTo toDto(News news); + + @Mapping(target = "id", ignore = true) + @Mapping(target = "created", ignore = true) + @Mapping(target = "modified", ignore = true) + void updateEntityFromDto(NewsRequestTo request, @MappingTarget News news); +} \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/mapper/UserMapper.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/mapper/UserMapper.java new file mode 100644 index 000000000..f09de36bb --- /dev/null +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/mapper/UserMapper.java @@ -0,0 +1,17 @@ +package org.example.newsapi.mapper; + +import org.example.newsapi.dto.request.UserRequestTo; +import org.example.newsapi.dto.response.UserResponseTo; +import org.example.newsapi.entity.User; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; + +@Mapper(componentModel = "spring") +public interface UserMapper { + User toEntity(UserRequestTo request); + UserResponseTo toDto(User user); + + @Mapping(target = "id", ignore = true) + void updateEntityFromDto(UserRequestTo request, @MappingTarget User user); +} \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/CrudRepository.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/CrudRepository.java new file mode 100644 index 000000000..1daacb88c --- /dev/null +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/CrudRepository.java @@ -0,0 +1,12 @@ +package org.example.newsapi.repository; + +import java.util.List; +import java.util.Optional; + +public interface CrudRepository { + T save(T entity); + Optional findById(ID id); + List findAll(); + T update(T entity); + void deleteById(ID id); +} \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/InMemoryRepository.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/InMemoryRepository.java new file mode 100644 index 000000000..0f383ed1c --- /dev/null +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/InMemoryRepository.java @@ -0,0 +1,46 @@ +package org.example.newsapi.repository; + +import org.example.newsapi.entity.BaseEntity; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; + +public abstract class InMemoryRepository implements CrudRepository { + + protected final Map storage = new ConcurrentHashMap<>(); + protected final AtomicLong idGenerator = new AtomicLong(0); + + @Override + public T save(T entity) { + if (entity.getId() == null) { + entity.setId(idGenerator.incrementAndGet()); + } + storage.put(entity.getId(), entity); + return entity; + } + + @Override + public Optional findById(Long id) { + return Optional.ofNullable(storage.get(id)); + } + + @Override + public List findAll() { + return new ArrayList<>(storage.values()); + } + + @Override + public T update(T entity) { + // В Map put заменяет значение, если ключ существует + storage.put(entity.getId(), entity); + return entity; + } + + @Override + public void deleteById(Long id) { + storage.remove(id); + } +} \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/impl/CommentRepository.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/impl/CommentRepository.java new file mode 100644 index 000000000..380f5a38e --- /dev/null +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/impl/CommentRepository.java @@ -0,0 +1,9 @@ +package org.example.newsapi.repository.impl; + +import org.example.newsapi.entity.Comment; +import org.example.newsapi.repository.InMemoryRepository; +import org.springframework.stereotype.Repository; + +@Repository +public class CommentRepository extends InMemoryRepository { +} \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/impl/MarkerRepository.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/impl/MarkerRepository.java new file mode 100644 index 000000000..3a2ec35a0 --- /dev/null +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/impl/MarkerRepository.java @@ -0,0 +1,9 @@ +package org.example.newsapi.repository.impl; + +import org.example.newsapi.entity.Marker; +import org.example.newsapi.repository.InMemoryRepository; +import org.springframework.stereotype.Repository; + +@Repository +public class MarkerRepository extends InMemoryRepository { +} \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/impl/NewsRepository.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/impl/NewsRepository.java new file mode 100644 index 000000000..d7b40b551 --- /dev/null +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/impl/NewsRepository.java @@ -0,0 +1,9 @@ +package org.example.newsapi.repository.impl; + +import org.example.newsapi.entity.News; +import org.example.newsapi.repository.InMemoryRepository; +import org.springframework.stereotype.Repository; + +@Repository +public class NewsRepository extends InMemoryRepository { +} \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/impl/UserRepository.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/impl/UserRepository.java new file mode 100644 index 000000000..5bf88e317 --- /dev/null +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/impl/UserRepository.java @@ -0,0 +1,9 @@ +package org.example.newsapi.repository.impl; + +import org.example.newsapi.entity.User; +import org.example.newsapi.repository.InMemoryRepository; +import org.springframework.stereotype.Repository; + +@Repository +public class UserRepository extends InMemoryRepository { +} \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/service/CommentService.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/service/CommentService.java new file mode 100644 index 000000000..d500bb036 --- /dev/null +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/service/CommentService.java @@ -0,0 +1,59 @@ +package org.example.newsapi.service; + + +import lombok.RequiredArgsConstructor; +import org.example.newsapi.dto.request.CommentRequestTo; +import org.example.newsapi.dto.response.CommentResponseTo; +import org.example.newsapi.entity.Comment; +import org.example.newsapi.exception.NotFoundException; +import org.example.newsapi.mapper.CommentMapper; +import org.example.newsapi.repository.impl.CommentRepository; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class CommentService { + private final CommentRepository commentRepository; + private final CommentMapper commentMapper; + + public CommentResponseTo create(CommentRequestTo request) { + Comment comment = commentMapper.toEntity(request); + return commentMapper.toDto(commentRepository.save(comment)); + } + + public List findAll() { + return commentRepository.findAll().stream() + .map(commentMapper::toDto) + .collect(Collectors.toList()); + } + + public CommentResponseTo findById(Long id) { + return commentRepository.findById(id) + .map(commentMapper::toDto) + .orElseThrow(() -> new NotFoundException("Comment not found with id: " + id)); + } + + public CommentResponseTo update(Long id, CommentRequestTo request) { + Comment comment = commentRepository.findById(id) + .orElseThrow(() -> new NotFoundException("Comment not found with id: " + id)); + commentMapper.updateEntityFromDto(request, comment); + return commentMapper.toDto(commentRepository.update(comment)); + } + + public void delete(Long id) { + if (commentRepository.findById(id).isEmpty()) { + throw new NotFoundException("Comment not found with id: " + id); + } + commentRepository.deleteById(id); + } + public List findByNewsId(Long newsId) { + return commentRepository.findAll().stream() + .filter(c -> c.getNewsId().equals(newsId)) + .map(commentMapper::toDto) + .collect(Collectors.toList()); + } + +} \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/service/MarkerService.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/service/MarkerService.java new file mode 100644 index 000000000..9f85dfa4d --- /dev/null +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/service/MarkerService.java @@ -0,0 +1,54 @@ +package org.example.newsapi.service; + + +import lombok.RequiredArgsConstructor; +import org.example.newsapi.dto.request.MarkerRequestTo; +import org.example.newsapi.dto.response.MarkerResponseTo; +import org.example.newsapi.entity.Marker; +import org.example.newsapi.exception.NotFoundException; +import org.example.newsapi.mapper.MarkerMapper; +import org.example.newsapi.mapper.MarkerMapper; +import org.example.newsapi.repository.impl.MarkerRepository; +import org.example.newsapi.repository.impl.MarkerRepository; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class MarkerService { + private final MarkerRepository markerRepository; + private final MarkerMapper markerMapper; + + public MarkerResponseTo create(MarkerRequestTo request) { + Marker marker = markerMapper.toEntity(request); + return markerMapper.toDto(markerRepository.save(marker)); + } + + public List findAll() { + return markerRepository.findAll().stream() + .map(markerMapper::toDto) + .collect(Collectors.toList()); + } + + public MarkerResponseTo findById(Long id) { + return markerRepository.findById(id) + .map(markerMapper::toDto) + .orElseThrow(() -> new NotFoundException("Marker not found with id: " + id)); + } + + public MarkerResponseTo update(Long id, MarkerRequestTo request) { + Marker marker = markerRepository.findById(id) + .orElseThrow(() -> new NotFoundException("Marker not found with id: " + id)); + markerMapper.updateEntityFromDto(request, marker); + return markerMapper.toDto(markerRepository.update(marker)); + } + + public void delete(Long id) { + if (markerRepository.findById(id).isEmpty()) { + throw new NotFoundException("Marker not found with id: " + id); + } + markerRepository.deleteById(id); + } +} \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/service/NewsService.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/service/NewsService.java new file mode 100644 index 000000000..4497806e0 --- /dev/null +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/service/NewsService.java @@ -0,0 +1,61 @@ +package org.example.newsapi.service; + + +import lombok.RequiredArgsConstructor; +import org.example.newsapi.dto.request.NewsRequestTo; +import org.example.newsapi.dto.response.NewsResponseTo; +import org.example.newsapi.entity.News; +import org.example.newsapi.exception.NotFoundException; +import org.example.newsapi.mapper.NewsMapper; +import org.example.newsapi.repository.impl.NewsRepository; +import org.example.newsapi.repository.impl.UserRepository; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class NewsService { + private final NewsRepository newsRepository; + private final UserRepository userRepository; + private final NewsMapper newsMapper; + + public NewsResponseTo create(NewsRequestTo request) { + // Валидация существования юзера + userRepository.findById(request.getUserId()) + .orElseThrow(() -> new NotFoundException("User not found")); + + News news = newsMapper.toEntity(request); + news.setCreated(LocalDateTime.now()); + news.setModified(LocalDateTime.now()); + return newsMapper.toDto(newsRepository.save(news)); + } + + public List findAll() { + return newsRepository.findAll().stream() + .map(newsMapper::toDto) + .collect(Collectors.toList()); + } + + public NewsResponseTo findById(Long id) { + return newsRepository.findById(id) + .map(newsMapper::toDto) + .orElseThrow(() -> new NotFoundException("News not found with id: " + id)); + } + + public NewsResponseTo update(Long id, NewsRequestTo request) { + News news = newsRepository.findById(id) + .orElseThrow(() -> new NotFoundException("News not found with id: " + id)); + newsMapper.updateEntityFromDto(request, news); + return newsMapper.toDto(newsRepository.update(news)); + } + + public void delete(Long id) { + if (newsRepository.findById(id).isEmpty()) { + throw new NotFoundException("News not found with id: " + id); + } + newsRepository.deleteById(id); + } +} \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/service/UserService.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/service/UserService.java new file mode 100644 index 000000000..d6bc1eddc --- /dev/null +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/service/UserService.java @@ -0,0 +1,52 @@ +package org.example.newsapi.service; + + +import org.example.newsapi.dto.request.UserRequestTo; +import org.example.newsapi.dto.response.UserResponseTo; +import org.example.newsapi.entity.User; +import org.example.newsapi.exception.NotFoundException; +import org.example.newsapi.mapper.UserMapper; +import org.example.newsapi.repository.impl.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class UserService { + private final UserRepository userRepository; + private final UserMapper userMapper; + + public UserResponseTo create(UserRequestTo request) { + User user = userMapper.toEntity(request); + return userMapper.toDto(userRepository.save(user)); + } + + public List findAll() { + return userRepository.findAll().stream() + .map(userMapper::toDto) + .collect(Collectors.toList()); + } + + public UserResponseTo findById(Long id) { + return userRepository.findById(id) + .map(userMapper::toDto) + .orElseThrow(() -> new NotFoundException("User not found with id: " + id)); + } + + public UserResponseTo update(Long id, UserRequestTo request) { + User user = userRepository.findById(id) + .orElseThrow(() -> new NotFoundException("User not found with id: " + id)); + userMapper.updateEntityFromDto(request, user); + return userMapper.toDto(userRepository.update(user)); + } + + public void delete(Long id) { + if (userRepository.findById(id).isEmpty()) { + throw new NotFoundException("User not found with id: " + id); + } + userRepository.deleteById(id); + } +} \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/resources/application.properties b/351004/Brazhalovich/Lab_1/src/main/resources/application.properties new file mode 100644 index 000000000..d0d376485 --- /dev/null +++ b/351004/Brazhalovich/Lab_1/src/main/resources/application.properties @@ -0,0 +1 @@ +server.port=24110 \ No newline at end of file From 32d680b01fc370f50d9083aba444a01899c8dd4b Mon Sep 17 00:00:00 2001 From: sasha Date: Tue, 24 Feb 2026 20:04:30 +0300 Subject: [PATCH 2/7] jpa without "Jdbc Check Marker (Wait 3 Markers in News with id=20)" --- 351004/Brazhalovich/Lab_1/pom.xml | 40 +++++++++- .../newsapi/controller/CommentController.java | 13 ++-- .../newsapi/controller/MarkerController.java | 10 ++- .../newsapi/controller/NewsController.java | 8 +- .../newsapi/controller/UserController.java | 13 +++- .../newsapi/dto/request/CommentRequestTo.java | 3 + .../newsapi/dto/request/MarkerRequestTo.java | 6 ++ .../newsapi/dto/request/NewsRequestTo.java | 3 + .../dto/response/CommentResponseTo.java | 5 ++ .../newsapi/dto/response/NewsResponseTo.java | 12 ++- .../newsapi/dto/response/UserResponseTo.java | 4 + .../example/newsapi/entity/BaseEntity.java | 9 --- .../org/example/newsapi/entity/Comment.java | 25 ++++-- .../org/example/newsapi/entity/Marker.java | 19 ++++- .../java/org/example/newsapi/entity/News.java | 52 +++++++++++-- .../java/org/example/newsapi/entity/User.java | 33 +++++++- .../exception/GlobalExceptionHandler.java | 37 ++++++--- .../example/newsapi/mapper/CommentMapper.java | 9 ++- .../example/newsapi/mapper/MarkerMapper.java | 6 +- .../example/newsapi/mapper/NewsMapper.java | 15 +++- .../example/newsapi/mapper/UserMapper.java | 5 ++ .../newsapi/repository/CrudRepository.java | 12 --- .../repository/InMemoryRepository.java | 46 ----------- .../repository/impl/CommentRepository.java | 9 --- .../repository/impl/MarkerRepository.java | 9 --- .../repository/impl/NewsRepository.java | 9 --- .../repository/impl/UserRepository.java | 9 --- .../newsapi/service/CommentService.java | 50 +++++++----- .../newsapi/service/MarkerService.java | 33 ++++---- .../example/newsapi/service/NewsService.java | 78 ++++++++++++++----- .../example/newsapi/service/UserService.java | 32 +++++--- .../src/main/resources/application.properties | 12 ++- 32 files changed, 405 insertions(+), 221 deletions(-) delete mode 100644 351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/entity/BaseEntity.java delete mode 100644 351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/CrudRepository.java delete mode 100644 351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/InMemoryRepository.java delete mode 100644 351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/impl/CommentRepository.java delete mode 100644 351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/impl/MarkerRepository.java delete mode 100644 351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/impl/NewsRepository.java delete mode 100644 351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/impl/UserRepository.java diff --git a/351004/Brazhalovich/Lab_1/pom.xml b/351004/Brazhalovich/Lab_1/pom.xml index ecb26433e..aa3c8eba7 100644 --- a/351004/Brazhalovich/Lab_1/pom.xml +++ b/351004/Brazhalovich/Lab_1/pom.xml @@ -12,8 +12,10 @@ org.example - untitled + newsapi 1.0-SNAPSHOT + newsapi + REST API with JPA and Liquibase 21 @@ -28,6 +30,25 @@ spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + + org.postgresql + postgresql + runtime + + + + + org.liquibase + liquibase-core + + org.springframework.boot @@ -60,6 +81,23 @@ rest-assured test + + + + org.springframework.boot + spring-boot-testcontainers + test + + + org.testcontainers + junit-jupiter + test + + + org.testcontainers + postgresql + test + diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/controller/CommentController.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/controller/CommentController.java index 9e8e7a469..7e740fae1 100644 --- a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/controller/CommentController.java +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/controller/CommentController.java @@ -5,6 +5,9 @@ import org.example.newsapi.dto.request.CommentRequestTo; import org.example.newsapi.dto.response.CommentResponseTo; import org.example.newsapi.service.CommentService; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; @@ -14,6 +17,7 @@ @RequestMapping("/api/v1.0/comments") @RequiredArgsConstructor public class CommentController { + private final CommentService commentService; @PostMapping @@ -23,8 +27,8 @@ public CommentResponseTo create(@RequestBody @Valid CommentRequestTo request) { } @GetMapping - public List getAll() { - return commentService.findAll(); + public List getAll(@PageableDefault(size = 50) Pageable pageable) { + return commentService.findAll(pageable).getContent(); } @GetMapping("/{id}") @@ -42,9 +46,4 @@ public CommentResponseTo update(@PathVariable Long id, @RequestBody @Valid Comme public void delete(@PathVariable Long id) { commentService.delete(id); } - @GetMapping("/by-news/{newsId}") - public List getByNewsId(@PathVariable Long newsId) { - return commentService.findByNewsId(newsId); - } - } \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/controller/MarkerController.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/controller/MarkerController.java index c24dc4bcb..107d89e87 100644 --- a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/controller/MarkerController.java +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/controller/MarkerController.java @@ -5,6 +5,9 @@ import org.example.newsapi.dto.request.MarkerRequestTo; import org.example.newsapi.dto.response.MarkerResponseTo; import org.example.newsapi.service.MarkerService; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; @@ -14,17 +17,18 @@ @RequestMapping("/api/v1.0/markers") @RequiredArgsConstructor public class MarkerController { + private final MarkerService markerService; @PostMapping @ResponseStatus(HttpStatus.CREATED) - public MarkerResponseTo create(@RequestBody @Valid MarkerRequestTo request) { + public MarkerResponseTo create(@RequestBody @Valid MarkerRequestTo request) { // ПРОВЕРЬ @Valid return markerService.create(request); } @GetMapping - public List getAll() { - return markerService.findAll(); + public List getAll(@PageableDefault(size = 50) Pageable pageable) { + return markerService.findAll(pageable).getContent(); } @GetMapping("/{id}") diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/controller/NewsController.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/controller/NewsController.java index df2b53c88..829ff1f34 100644 --- a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/controller/NewsController.java +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/controller/NewsController.java @@ -5,6 +5,9 @@ import org.example.newsapi.dto.request.NewsRequestTo; import org.example.newsapi.dto.response.NewsResponseTo; import org.example.newsapi.service.NewsService; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; @@ -14,6 +17,7 @@ @RequestMapping("/api/v1.0/news") @RequiredArgsConstructor public class NewsController { + private final NewsService newsService; @PostMapping @@ -23,8 +27,8 @@ public NewsResponseTo create(@RequestBody @Valid NewsRequestTo request) { } @GetMapping - public List getAll() { - return newsService.findAll(); + public List getAll(@PageableDefault(size = 50) Pageable pageable) { + return newsService.findAll(pageable).getContent(); } @GetMapping("/{id}") diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/controller/UserController.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/controller/UserController.java index fa4a33fd6..e2ea47fae 100644 --- a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/controller/UserController.java +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/controller/UserController.java @@ -1,10 +1,13 @@ package org.example.newsapi.controller; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; import org.example.newsapi.dto.request.UserRequestTo; import org.example.newsapi.dto.response.UserResponseTo; import org.example.newsapi.service.UserService; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; @@ -14,6 +17,7 @@ @RequestMapping("/api/v1.0/users") @RequiredArgsConstructor public class UserController { + private final UserService userService; @PostMapping @@ -23,8 +27,9 @@ public UserResponseTo create(@RequestBody @Valid UserRequestTo request) { } @GetMapping - public List getAll() { - return userService.findAll(); + public List getAll(@PageableDefault(size = 50) Pageable pageable) { + // Возвращаем только контент списка, чтобы тестер не путался в мета-данных Page + return userService.findAll(pageable).getContent(); } @GetMapping("/{id}") diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/request/CommentRequestTo.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/request/CommentRequestTo.java index 630ea739f..d61b71f22 100644 --- a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/request/CommentRequestTo.java +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/request/CommentRequestTo.java @@ -1,11 +1,14 @@ package org.example.newsapi.dto.request; +import com.fasterxml.jackson.annotation.JsonProperty; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; import lombok.Data; @Data public class CommentRequestTo { + + //@JsonProperty("news") @NotNull private Long newsId; diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/request/MarkerRequestTo.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/request/MarkerRequestTo.java index fe0cd99c3..6f3e24e02 100644 --- a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/request/MarkerRequestTo.java +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/request/MarkerRequestTo.java @@ -1,10 +1,16 @@ package org.example.newsapi.dto.request; +import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; @Data +@NoArgsConstructor // ОБЯЗАТЕЛЬНО +@AllArgsConstructor public class MarkerRequestTo { + @NotBlank @Size(min = 2, max = 32) private String name; } \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/request/NewsRequestTo.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/request/NewsRequestTo.java index f117a445f..e95328739 100644 --- a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/request/NewsRequestTo.java +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/request/NewsRequestTo.java @@ -1,5 +1,6 @@ package org.example.newsapi.dto.request; +import com.fasterxml.jackson.annotation.JsonProperty; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; import lombok.Data; @@ -7,6 +8,7 @@ @Data public class NewsRequestTo { + //@JsonProperty("user") // Тестер будет слать "user", а Jackson запишет это в userId @NotNull private Long userId; @@ -16,5 +18,6 @@ public class NewsRequestTo { @Size(min = 4, max = 2048) private String content; + //@JsonProperty("marker") private Set markerIds; } \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/response/CommentResponseTo.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/response/CommentResponseTo.java index b7dac9a85..b5aa9de31 100644 --- a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/response/CommentResponseTo.java +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/response/CommentResponseTo.java @@ -1,10 +1,15 @@ package org.example.newsapi.dto.response; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; @Data public class CommentResponseTo { + private Long id; + + //@JsonProperty("news") private Long newsId; + private String content; } \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/response/NewsResponseTo.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/response/NewsResponseTo.java index 2b183223b..6ef64d7bf 100644 --- a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/response/NewsResponseTo.java +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/response/NewsResponseTo.java @@ -1,16 +1,26 @@ package org.example.newsapi.dto.response; +import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; import java.time.LocalDateTime; +import java.util.HashSet; import java.util.Set; @Data public class NewsResponseTo { private Long id; + + + //@JsonProperty("user") + //@JsonAlias({"userId", "user"}) private Long userId; + private String title; private String content; private LocalDateTime created; private LocalDateTime modified; - private Set markerIds; // Для упрощения пока возвращаем ID + + //@JsonProperty("marker") + private Set markerIds = new HashSet<>(); } \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/response/UserResponseTo.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/response/UserResponseTo.java index 5c8e1d964..164b19ed7 100644 --- a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/response/UserResponseTo.java +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/response/UserResponseTo.java @@ -1,8 +1,12 @@ package org.example.newsapi.dto.response; +import com.fasterxml.jackson.annotation.JsonTypeName; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonRootName; import lombok.Data; @Data +@JsonRootName("user") // Если тест требует обертку public class UserResponseTo { private Long id; private String login; diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/entity/BaseEntity.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/entity/BaseEntity.java deleted file mode 100644 index d6222bb59..000000000 --- a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/entity/BaseEntity.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.example.newsapi.entity; - -import lombok.Data; -import java.io.Serializable; - -@Data -public abstract class BaseEntity implements Serializable { - private Long id; -} \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/entity/Comment.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/entity/Comment.java index 923a6525e..27dfab0f6 100644 --- a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/entity/Comment.java +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/entity/Comment.java @@ -1,11 +1,26 @@ package org.example.newsapi.entity; -import lombok.Data; -import lombok.EqualsAndHashCode; +import jakarta.persistence.*; +import lombok.*; +@Entity +@Table(name = "tbl_comment") @Data -@EqualsAndHashCode(callSuper = true) -public class Comment extends BaseEntity { - private Long newsId; // FK +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class Comment { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "comment_seq_gen") + @SequenceGenerator(name = "comment_seq_gen", sequenceName = "comment_seq", allocationSize = 1) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "news_id", nullable = false) + @ToString.Exclude + private News news; + + @Column(nullable = false, length = 2048) private String content; } \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/entity/Marker.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/entity/Marker.java index d98c0ff3a..e61bc7936 100644 --- a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/entity/Marker.java +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/entity/Marker.java @@ -1,10 +1,21 @@ package org.example.newsapi.entity; -import lombok.Data; -import lombok.EqualsAndHashCode; +import jakarta.persistence.*; +import lombok.*; +@Entity +@Table(name = "tbl_marker") @Data -@EqualsAndHashCode(callSuper = true) -public class Marker extends BaseEntity { +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class Marker { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "marker_seq_gen") + @SequenceGenerator(name = "marker_seq_gen", sequenceName = "marker_seq", allocationSize = 1) + private Long id; + + @Column(unique = true, nullable = false, length = 32) private String name; } \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/entity/News.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/entity/News.java index 5b3d5f547..a7507d720 100644 --- a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/entity/News.java +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/entity/News.java @@ -1,19 +1,57 @@ package org.example.newsapi.entity; -import lombok.Data; -import lombok.EqualsAndHashCode; +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + import java.time.LocalDateTime; import java.util.HashSet; +import java.util.List; import java.util.Set; +@Entity +@Table(name = "tbl_news") @Data -@EqualsAndHashCode(callSuper = true) -public class News extends BaseEntity { - private Long userId; // FK +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class News { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "news_seq_gen") + @SequenceGenerator(name = "news_seq_gen", sequenceName = "news_seq", allocationSize = 1) + private Long id; + + // Связь многие-к-одному с User + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + @ToString.Exclude + private User user; + + @Column(nullable = false, length = 64) private String title; + + @Column(nullable = false, length = 2048) private String content; + + @CreationTimestamp // Автоматически ставит дату при создании private LocalDateTime created; + + @UpdateTimestamp // Автоматически обновляет дату при изменении private LocalDateTime modified; - // Many-to-Many имитация через хранение ID маркеров - private Set markerIds = new HashSet<>(); + + // Связь один-ко-многим с комментариями + @OneToMany(mappedBy = "news", cascade = CascadeType.ALL, orphanRemoval = true) + @ToString.Exclude + private List comments; + + // Связь многие-ко-многим с маркерами через промежуточную таблицу + @ManyToMany(fetch = FetchType.EAGER) // Поставь EAGER для тестов, чтобы маппер всегда видел маркеры + @JoinTable( + name = "tbl_news_marker", + joinColumns = @JoinColumn(name = "news_id"), + inverseJoinColumns = @JoinColumn(name = "marker_id") + ) + private Set markers = new HashSet<>(); } \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/entity/User.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/entity/User.java index 08ce0912f..b95952017 100644 --- a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/entity/User.java +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/entity/User.java @@ -1,13 +1,38 @@ package org.example.newsapi.entity; -import lombok.Data; -import lombok.EqualsAndHashCode; +import jakarta.persistence.*; +import lombok.*; +import java.util.List; + +@Entity +@Table(name = "tbl_user") @Data -@EqualsAndHashCode(callSuper = true) -public class User extends BaseEntity { +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "user_seq_gen") + @SequenceGenerator(name = "user_seq_gen", sequenceName = "user_seq", allocationSize = 1) + private Long id; + + @Column(nullable = false, unique = true, length = 64) private String login; + + @Column(nullable = false, length = 128) private String password; + + @Column(length = 64) private String firstname; + + @Column(length = 64) private String lastname; + + // Связь один-ко-многим с новостями + // mappedBy указывает на поле "user" в классе News + @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @ToString.Exclude // Важно! Исключаем из toString, чтобы не было рекурсии и переполнения стека + private List news; } \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/exception/GlobalExceptionHandler.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/exception/GlobalExceptionHandler.java index de469e2cf..18b39c852 100644 --- a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/exception/GlobalExceptionHandler.java +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/exception/GlobalExceptionHandler.java @@ -1,28 +1,47 @@ package org.example.newsapi.exception; + +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +import jakarta.validation.ConstraintViolationException; @RestControllerAdvice public class GlobalExceptionHandler { + // 1. Ошибки "Не найдено" (User, News и т.д.) -> Статус 403, Код 40301 @ExceptionHandler(NotFoundException.class) public ResponseEntity handleNotFound(NotFoundException e) { - // 404 + 01 (arbitrary suffix) - return ResponseEntity.status(HttpStatus.NOT_FOUND) - .body(new ErrorResponse(e.getMessage(), 40401)); + return ResponseEntity.status(HttpStatus.FORBIDDEN) + .body(new ErrorResponse(e.getMessage(), 40301)); + } + + // 2. Ошибки дубликатов (логин занят) -> Статус 403, Код 40301 + @ExceptionHandler(AlreadyExistsException.class) + public ResponseEntity handleAlreadyExists(AlreadyExistsException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN) + .body(new ErrorResponse(e.getMessage(), 40301)); + } + + // 3. Ошибки базы данных (те самые SQL Error: 23505 и 22001 из твоего лога) + @ExceptionHandler(org.springframework.dao.DataIntegrityViolationException.class) + public ResponseEntity handleDataIntegrity(Exception e) { + // Любая ошибка БД (например, попытка создать новость с несуществующим userId, + // если проверка в сервисе вдруг пропустила) -> 403 + return ResponseEntity.status(HttpStatus.FORBIDDEN) + .body(new ErrorResponse("Integrity violation", 40301)); } - @ExceptionHandler(MethodArgumentNotValidException.class) - public ResponseEntity handleValidation(MethodArgumentNotValidException e) { - String message = e.getBindingResult().getAllErrors().get(0).getDefaultMessage(); - // 400 + 01 - return ResponseEntity.status(HttpStatus.BAD_REQUEST) - .body(new ErrorResponse("Validation error: " + message, 40001)); + // 4. Ошибки валидации (аннотации @Size, @NotBlank) -> Статус 403, Код 40301 + @ExceptionHandler({MethodArgumentNotValidException.class, ConstraintViolationException.class}) + public ResponseEntity handleValidation(Exception e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN) + .body(new ErrorResponse("Validation error", 40301)); } + // 5. Все остальные непредвиденные ошибки -> 500 @ExceptionHandler(Exception.class) public ResponseEntity handleAll(Exception e) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/mapper/CommentMapper.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/mapper/CommentMapper.java index bd5ac4aa2..ba3e2255f 100644 --- a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/mapper/CommentMapper.java +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/mapper/CommentMapper.java @@ -1,20 +1,23 @@ package org.example.newsapi.mapper; import org.example.newsapi.dto.request.CommentRequestTo; -import org.example.newsapi.dto.request.MarkerRequestTo; import org.example.newsapi.dto.response.CommentResponseTo; -import org.example.newsapi.dto.response.MarkerResponseTo; import org.example.newsapi.entity.Comment; -import org.example.newsapi.entity.Marker; import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.MappingTarget; @Mapper(componentModel = "spring") public interface CommentMapper { + + @Mapping(target = "id", ignore = true) + @Mapping(target = "news", ignore = true) // Заполняется в CommentService Comment toEntity(CommentRequestTo request); + + @Mapping(target = "newsId", source = "news.id") CommentResponseTo toDto(Comment comment); @Mapping(target = "id", ignore = true) + @Mapping(target = "news", ignore = true) void updateEntityFromDto(CommentRequestTo request, @MappingTarget Comment comment); } \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/mapper/MarkerMapper.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/mapper/MarkerMapper.java index 6e4d2ebf7..ea739545d 100644 --- a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/mapper/MarkerMapper.java +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/mapper/MarkerMapper.java @@ -1,18 +1,18 @@ package org.example.newsapi.mapper; import org.example.newsapi.dto.request.MarkerRequestTo; -import org.example.newsapi.dto.request.UserRequestTo; import org.example.newsapi.dto.response.MarkerResponseTo; -import org.example.newsapi.dto.response.UserResponseTo; import org.example.newsapi.entity.Marker; -import org.example.newsapi.entity.User; import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.MappingTarget; @Mapper(componentModel = "spring") public interface MarkerMapper { + + @Mapping(target = "id", ignore = true) Marker toEntity(MarkerRequestTo request); + MarkerResponseTo toDto(Marker marker); @Mapping(target = "id", ignore = true) diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/mapper/NewsMapper.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/mapper/NewsMapper.java index b66a9d1cb..62d3e4608 100644 --- a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/mapper/NewsMapper.java +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/mapper/NewsMapper.java @@ -2,22 +2,35 @@ import org.example.newsapi.dto.request.NewsRequestTo; import org.example.newsapi.dto.response.NewsResponseTo; +import org.example.newsapi.entity.Marker; import org.example.newsapi.entity.News; import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.MappingTarget; -@Mapper(componentModel = "spring") +import java.util.Set; +import java.util.stream.Collectors; + +@Mapper(componentModel = "spring", imports = {Collectors.class, Set.class, Marker.class}) public interface NewsMapper { + @Mapping(target = "id", ignore = true) + @Mapping(target = "user", ignore = true) + @Mapping(target = "markers", ignore = true) @Mapping(target = "created", ignore = true) @Mapping(target = "modified", ignore = true) + @Mapping(target = "comments", ignore = true) News toEntity(NewsRequestTo request); + @Mapping(target = "userId", source = "user.id") + @Mapping(target = "markerIds", expression = "java(news.getMarkers() != null ? news.getMarkers().stream().map(Marker::getId).collect(Collectors.toSet()) : new java.util.HashSet<>())") NewsResponseTo toDto(News news); @Mapping(target = "id", ignore = true) + @Mapping(target = "user", ignore = true) + @Mapping(target = "markers", ignore = true) @Mapping(target = "created", ignore = true) @Mapping(target = "modified", ignore = true) + @Mapping(target = "comments", ignore = true) void updateEntityFromDto(NewsRequestTo request, @MappingTarget News news); } \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/mapper/UserMapper.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/mapper/UserMapper.java index f09de36bb..e3572b9c8 100644 --- a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/mapper/UserMapper.java +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/mapper/UserMapper.java @@ -9,9 +9,14 @@ @Mapper(componentModel = "spring") public interface UserMapper { + + @Mapping(target = "id", ignore = true) + @Mapping(target = "news", ignore = true) // Игнорируем список новостей при создании User toEntity(UserRequestTo request); + UserResponseTo toDto(User user); @Mapping(target = "id", ignore = true) + @Mapping(target = "news", ignore = true) void updateEntityFromDto(UserRequestTo request, @MappingTarget User user); } \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/CrudRepository.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/CrudRepository.java deleted file mode 100644 index 1daacb88c..000000000 --- a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/CrudRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.example.newsapi.repository; - -import java.util.List; -import java.util.Optional; - -public interface CrudRepository { - T save(T entity); - Optional findById(ID id); - List findAll(); - T update(T entity); - void deleteById(ID id); -} \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/InMemoryRepository.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/InMemoryRepository.java deleted file mode 100644 index 0f383ed1c..000000000 --- a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/InMemoryRepository.java +++ /dev/null @@ -1,46 +0,0 @@ -package org.example.newsapi.repository; - -import org.example.newsapi.entity.BaseEntity; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicLong; - -public abstract class InMemoryRepository implements CrudRepository { - - protected final Map storage = new ConcurrentHashMap<>(); - protected final AtomicLong idGenerator = new AtomicLong(0); - - @Override - public T save(T entity) { - if (entity.getId() == null) { - entity.setId(idGenerator.incrementAndGet()); - } - storage.put(entity.getId(), entity); - return entity; - } - - @Override - public Optional findById(Long id) { - return Optional.ofNullable(storage.get(id)); - } - - @Override - public List findAll() { - return new ArrayList<>(storage.values()); - } - - @Override - public T update(T entity) { - // В Map put заменяет значение, если ключ существует - storage.put(entity.getId(), entity); - return entity; - } - - @Override - public void deleteById(Long id) { - storage.remove(id); - } -} \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/impl/CommentRepository.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/impl/CommentRepository.java deleted file mode 100644 index 380f5a38e..000000000 --- a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/impl/CommentRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.example.newsapi.repository.impl; - -import org.example.newsapi.entity.Comment; -import org.example.newsapi.repository.InMemoryRepository; -import org.springframework.stereotype.Repository; - -@Repository -public class CommentRepository extends InMemoryRepository { -} \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/impl/MarkerRepository.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/impl/MarkerRepository.java deleted file mode 100644 index 3a2ec35a0..000000000 --- a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/impl/MarkerRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.example.newsapi.repository.impl; - -import org.example.newsapi.entity.Marker; -import org.example.newsapi.repository.InMemoryRepository; -import org.springframework.stereotype.Repository; - -@Repository -public class MarkerRepository extends InMemoryRepository { -} \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/impl/NewsRepository.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/impl/NewsRepository.java deleted file mode 100644 index d7b40b551..000000000 --- a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/impl/NewsRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.example.newsapi.repository.impl; - -import org.example.newsapi.entity.News; -import org.example.newsapi.repository.InMemoryRepository; -import org.springframework.stereotype.Repository; - -@Repository -public class NewsRepository extends InMemoryRepository { -} \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/impl/UserRepository.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/impl/UserRepository.java deleted file mode 100644 index 5bf88e317..000000000 --- a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/impl/UserRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.example.newsapi.repository.impl; - -import org.example.newsapi.entity.User; -import org.example.newsapi.repository.InMemoryRepository; -import org.springframework.stereotype.Repository; - -@Repository -public class UserRepository extends InMemoryRepository { -} \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/service/CommentService.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/service/CommentService.java index d500bb036..3aa80985a 100644 --- a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/service/CommentService.java +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/service/CommentService.java @@ -1,33 +1,43 @@ package org.example.newsapi.service; - import lombok.RequiredArgsConstructor; import org.example.newsapi.dto.request.CommentRequestTo; import org.example.newsapi.dto.response.CommentResponseTo; import org.example.newsapi.entity.Comment; +import org.example.newsapi.entity.News; import org.example.newsapi.exception.NotFoundException; import org.example.newsapi.mapper.CommentMapper; -import org.example.newsapi.repository.impl.CommentRepository; +import org.example.newsapi.repository.CommentRepository; +import org.example.newsapi.repository.NewsRepository; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; - -import java.util.List; -import java.util.stream.Collectors; +import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor +@Transactional(readOnly = true) public class CommentService { + private final CommentRepository commentRepository; + private final NewsRepository newsRepository; private final CommentMapper commentMapper; + @Transactional public CommentResponseTo create(CommentRequestTo request) { + // Находим новость, к которой пишется комментарий + News news = newsRepository.findById(request.getNewsId()) + .orElseThrow(() -> new NotFoundException("News not found with id: " + request.getNewsId())); + Comment comment = commentMapper.toEntity(request); + comment.setNews(news); + return commentMapper.toDto(commentRepository.save(comment)); } - public List findAll() { - return commentRepository.findAll().stream() - .map(commentMapper::toDto) - .collect(Collectors.toList()); + public Page findAll(Pageable pageable) { + return commentRepository.findAll(pageable) + .map(commentMapper::toDto); } public CommentResponseTo findById(Long id) { @@ -36,24 +46,28 @@ public CommentResponseTo findById(Long id) { .orElseThrow(() -> new NotFoundException("Comment not found with id: " + id)); } + @Transactional public CommentResponseTo update(Long id, CommentRequestTo request) { Comment comment = commentRepository.findById(id) .orElseThrow(() -> new NotFoundException("Comment not found with id: " + id)); + commentMapper.updateEntityFromDto(request, comment); - return commentMapper.toDto(commentRepository.update(comment)); + + // Если меняется привязка к новости (редкий кейс, но возможный) + if (request.getNewsId() != null && !request.getNewsId().equals(comment.getNews().getId())) { + News news = newsRepository.findById(request.getNewsId()) + .orElseThrow(() -> new NotFoundException("News not found")); + comment.setNews(news); + } + + return commentMapper.toDto(commentRepository.save(comment)); } + @Transactional public void delete(Long id) { - if (commentRepository.findById(id).isEmpty()) { + if (!commentRepository.existsById(id)) { throw new NotFoundException("Comment not found with id: " + id); } commentRepository.deleteById(id); } - public List findByNewsId(Long newsId) { - return commentRepository.findAll().stream() - .filter(c -> c.getNewsId().equals(newsId)) - .map(commentMapper::toDto) - .collect(Collectors.toList()); - } - } \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/service/MarkerService.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/service/MarkerService.java index 9f85dfa4d..2d2e4d229 100644 --- a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/service/MarkerService.java +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/service/MarkerService.java @@ -1,35 +1,37 @@ package org.example.newsapi.service; - import lombok.RequiredArgsConstructor; import org.example.newsapi.dto.request.MarkerRequestTo; import org.example.newsapi.dto.response.MarkerResponseTo; import org.example.newsapi.entity.Marker; import org.example.newsapi.exception.NotFoundException; import org.example.newsapi.mapper.MarkerMapper; -import org.example.newsapi.mapper.MarkerMapper; -import org.example.newsapi.repository.impl.MarkerRepository; -import org.example.newsapi.repository.impl.MarkerRepository; +import org.example.newsapi.repository.MarkerRepository; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; - -import java.util.List; -import java.util.stream.Collectors; +import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor +@Transactional(readOnly = true) public class MarkerService { + private final MarkerRepository markerRepository; private final MarkerMapper markerMapper; + @Transactional public MarkerResponseTo create(MarkerRequestTo request) { Marker marker = markerMapper.toEntity(request); - return markerMapper.toDto(markerRepository.save(marker)); + Marker saved = markerRepository.save(marker); + // Добавь лог, чтобы видеть в консоли, что маркер реально сохранился + System.out.println("Saved marker: " + saved.getName() + " with ID: " + saved.getId()); + return markerMapper.toDto(saved); } - public List findAll() { - return markerRepository.findAll().stream() - .map(markerMapper::toDto) - .collect(Collectors.toList()); + public Page findAll(Pageable pageable) { + return markerRepository.findAll(pageable) + .map(markerMapper::toDto); } public MarkerResponseTo findById(Long id) { @@ -38,15 +40,18 @@ public MarkerResponseTo findById(Long id) { .orElseThrow(() -> new NotFoundException("Marker not found with id: " + id)); } + @Transactional public MarkerResponseTo update(Long id, MarkerRequestTo request) { Marker marker = markerRepository.findById(id) .orElseThrow(() -> new NotFoundException("Marker not found with id: " + id)); + markerMapper.updateEntityFromDto(request, marker); - return markerMapper.toDto(markerRepository.update(marker)); + return markerMapper.toDto(markerRepository.save(marker)); } + @Transactional public void delete(Long id) { - if (markerRepository.findById(id).isEmpty()) { + if (!markerRepository.existsById(id)) { throw new NotFoundException("Marker not found with id: " + id); } markerRepository.deleteById(id); diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/service/NewsService.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/service/NewsService.java index 4497806e0..45fe62358 100644 --- a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/service/NewsService.java +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/service/NewsService.java @@ -1,60 +1,100 @@ package org.example.newsapi.service; - import lombok.RequiredArgsConstructor; import org.example.newsapi.dto.request.NewsRequestTo; import org.example.newsapi.dto.response.NewsResponseTo; +import org.example.newsapi.entity.Marker; import org.example.newsapi.entity.News; +import org.example.newsapi.entity.User; +import org.example.newsapi.exception.AlreadyExistsException; import org.example.newsapi.exception.NotFoundException; import org.example.newsapi.mapper.NewsMapper; -import org.example.newsapi.repository.impl.NewsRepository; -import org.example.newsapi.repository.impl.UserRepository; +import org.example.newsapi.repository.MarkerRepository; +import org.example.newsapi.repository.NewsRepository; +import org.example.newsapi.repository.UserRepository; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; +import java.util.HashSet; import java.util.List; -import java.util.stream.Collectors; @Service @RequiredArgsConstructor +@Transactional(readOnly = true) public class NewsService { + private final NewsRepository newsRepository; private final UserRepository userRepository; + private final MarkerRepository markerRepository; private final NewsMapper newsMapper; + @Transactional public NewsResponseTo create(NewsRequestTo request) { - // Валидация существования юзера - userRepository.findById(request.getUserId()) - .orElseThrow(() -> new NotFoundException("User not found")); + // Если userId null (т.е. тестер не прислал поле "user" или прислал "userId") + if (request.getUserId() == null || !userRepository.existsById(request.getUserId())) { + throw new NotFoundException("User not found or missing"); + } + + // Проверка уникальности заголовка (важно!) + if (newsRepository.existsByTitle(request.getTitle())) { + throw new AlreadyExistsException("News title already exists"); + } + User user = userRepository.getReferenceById(request.getUserId()); News news = newsMapper.toEntity(request); - news.setCreated(LocalDateTime.now()); - news.setModified(LocalDateTime.now()); - return newsMapper.toDto(newsRepository.save(news)); + news.setUser(user); + + // Принудительно ставим даты для ответа + news.setCreated(java.time.LocalDateTime.now()); + news.setModified(java.time.LocalDateTime.now()); + + if (request.getMarkerIds() != null && !request.getMarkerIds().isEmpty()) { + java.util.List markers = markerRepository.findAllById(request.getMarkerIds()); + news.setMarkers(new java.util.HashSet<>(markers)); + } + + News saved = newsRepository.save(news); + return newsMapper.toDto(saved); } - public List findAll() { - return newsRepository.findAll().stream() - .map(newsMapper::toDto) - .collect(Collectors.toList()); + public Page findAll(Pageable pageable) { + return newsRepository.findAll(pageable).map(newsMapper::toDto); } public NewsResponseTo findById(Long id) { return newsRepository.findById(id) .map(newsMapper::toDto) - .orElseThrow(() -> new NotFoundException("News not found with id: " + id)); + .orElseThrow(() -> new NotFoundException("News not found")); } + @Transactional public NewsResponseTo update(Long id, NewsRequestTo request) { News news = newsRepository.findById(id) - .orElseThrow(() -> new NotFoundException("News not found with id: " + id)); + .orElseThrow(() -> new NotFoundException("News not found")); + + if (!userRepository.existsById(request.getUserId())) { + throw new NotFoundException("User not found"); + } + newsMapper.updateEntityFromDto(request, news); - return newsMapper.toDto(newsRepository.update(news)); + news.setUser(userRepository.getReferenceById(request.getUserId())); + news.setModified(LocalDateTime.now()); + + if (request.getMarkerIds() != null) { + List markers = markerRepository.findAllById(request.getMarkerIds()); + news.setMarkers(new HashSet<>(markers)); + } + + return newsMapper.toDto(newsRepository.save(news)); } + @Transactional public void delete(Long id) { - if (newsRepository.findById(id).isEmpty()) { - throw new NotFoundException("News not found with id: " + id); + if (!newsRepository.existsById(id)) { + throw new NotFoundException("News not found"); } newsRepository.deleteById(id); } diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/service/UserService.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/service/UserService.java index d6bc1eddc..daa805e7a 100644 --- a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/service/UserService.java +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/service/UserService.java @@ -1,33 +1,38 @@ package org.example.newsapi.service; - +import lombok.RequiredArgsConstructor; import org.example.newsapi.dto.request.UserRequestTo; import org.example.newsapi.dto.response.UserResponseTo; import org.example.newsapi.entity.User; +import org.example.newsapi.exception.AlreadyExistsException; import org.example.newsapi.exception.NotFoundException; import org.example.newsapi.mapper.UserMapper; -import org.example.newsapi.repository.impl.UserRepository; -import lombok.RequiredArgsConstructor; +import org.example.newsapi.repository.UserRepository; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; - -import java.util.List; -import java.util.stream.Collectors; +import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor +@Transactional(readOnly = true) // По умолчанию транзакции только на чтение public class UserService { + private final UserRepository userRepository; private final UserMapper userMapper; + @Transactional public UserResponseTo create(UserRequestTo request) { + if (userRepository.existsByLogin(request.getLogin())) { + throw new AlreadyExistsException("Login already exists"); // Теперь это даст 403 + } User user = userMapper.toEntity(request); return userMapper.toDto(userRepository.save(user)); } - public List findAll() { - return userRepository.findAll().stream() - .map(userMapper::toDto) - .collect(Collectors.toList()); + public Page findAll(Pageable pageable) { + return userRepository.findAll(pageable) + .map(userMapper::toDto); } public UserResponseTo findById(Long id) { @@ -36,15 +41,18 @@ public UserResponseTo findById(Long id) { .orElseThrow(() -> new NotFoundException("User not found with id: " + id)); } + @Transactional public UserResponseTo update(Long id, UserRequestTo request) { User user = userRepository.findById(id) .orElseThrow(() -> new NotFoundException("User not found with id: " + id)); + userMapper.updateEntityFromDto(request, user); - return userMapper.toDto(userRepository.update(user)); + return userMapper.toDto(userRepository.save(user)); } + @Transactional public void delete(Long id) { - if (userRepository.findById(id).isEmpty()) { + if (!userRepository.existsById(id)) { throw new NotFoundException("User not found with id: " + id); } userRepository.deleteById(id); diff --git a/351004/Brazhalovich/Lab_1/src/main/resources/application.properties b/351004/Brazhalovich/Lab_1/src/main/resources/application.properties index d0d376485..473056974 100644 --- a/351004/Brazhalovich/Lab_1/src/main/resources/application.properties +++ b/351004/Brazhalovich/Lab_1/src/main/resources/application.properties @@ -1 +1,11 @@ -server.port=24110 \ No newline at end of file +server.port=24110 +spring.datasource.url=jdbc:postgresql://localhost:5432/distcomp +spring.datasource.username=postgres +spring.datasource.password=postgres +spring.datasource.driver-class-name=org.postgresql.Driver + +spring.jpa.hibernate.ddl-auto=update +spring.jpa.show-sql=true + +spring.liquibase.change-log=classpath:db/changelog/db.changelog-master.xml +spring.liquibase.enabled=true \ No newline at end of file From c06f0d310e2adb7805baadd683c47af020417e5b Mon Sep 17 00:00:00 2001 From: sasha Date: Tue, 24 Feb 2026 20:05:49 +0300 Subject: [PATCH 3/7] jpa without "Jdbc Check Marker (Wait 3 Markers in News with id=20)" --- .../exception/AlreadyExistsException.java | 7 + .../newsapi/repository/CommentRepository.java | 9 ++ .../newsapi/repository/MarkerRepository.java | 10 ++ .../newsapi/repository/NewsRepository.java | 12 ++ .../newsapi/repository/UserRepository.java | 12 ++ .../changeset/v1.0.0-create-tables.xml | 107 ++++++++++++++ .../db/changelog/db.changelog-master.xml | 10 ++ .../newsapi/AbstractIntegrationTest.java | 41 ++++++ .../controller/NewsControllerTest.java | 135 ++++++++++++++++++ .../test/resources/db/test-init-schema.sql | 1 + 10 files changed, 344 insertions(+) create mode 100644 351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/exception/AlreadyExistsException.java create mode 100644 351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/CommentRepository.java create mode 100644 351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/MarkerRepository.java create mode 100644 351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/NewsRepository.java create mode 100644 351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/UserRepository.java create mode 100644 351004/Brazhalovich/Lab_1/src/main/resources/db/changelog/changeset/v1.0.0-create-tables.xml create mode 100644 351004/Brazhalovich/Lab_1/src/main/resources/db/changelog/db.changelog-master.xml create mode 100644 351004/Brazhalovich/Lab_1/src/test/java/org/example/newsapi/AbstractIntegrationTest.java create mode 100644 351004/Brazhalovich/Lab_1/src/test/java/org/example/newsapi/controller/NewsControllerTest.java create mode 100644 351004/Brazhalovich/Lab_1/src/test/resources/db/test-init-schema.sql diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/exception/AlreadyExistsException.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/exception/AlreadyExistsException.java new file mode 100644 index 000000000..b25b1d5bf --- /dev/null +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/exception/AlreadyExistsException.java @@ -0,0 +1,7 @@ +package org.example.newsapi.exception; + +public class AlreadyExistsException extends RuntimeException { + public AlreadyExistsException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/CommentRepository.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/CommentRepository.java new file mode 100644 index 000000000..d68d94d29 --- /dev/null +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/CommentRepository.java @@ -0,0 +1,9 @@ +package org.example.newsapi.repository; + +import org.example.newsapi.entity.Comment; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface CommentRepository extends JpaRepository { +} \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/MarkerRepository.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/MarkerRepository.java new file mode 100644 index 000000000..c16187183 --- /dev/null +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/MarkerRepository.java @@ -0,0 +1,10 @@ +package org.example.newsapi.repository; + +import org.example.newsapi.entity.Marker; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface MarkerRepository extends JpaRepository { + boolean existsByName(String name); +} \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/NewsRepository.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/NewsRepository.java new file mode 100644 index 000000000..1494b1ba9 --- /dev/null +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/NewsRepository.java @@ -0,0 +1,12 @@ +package org.example.newsapi.repository; + +import org.example.newsapi.entity.News; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.stereotype.Repository; + +@Repository +public interface NewsRepository extends JpaRepository, JpaSpecificationExecutor { + // Добавь этот метод: + boolean existsByTitle(String title); +} \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/UserRepository.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/UserRepository.java new file mode 100644 index 000000000..8a05a98e1 --- /dev/null +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/UserRepository.java @@ -0,0 +1,12 @@ +package org.example.newsapi.repository; + +import org.example.newsapi.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface UserRepository extends JpaRepository { + // JpaRepository уже содержит методы findAll, findById, save, deleteById + // Дополнительные методы можно объявлять здесь (например, findByLogin), если понадобятся + boolean existsByLogin(String login); +} \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/resources/db/changelog/changeset/v1.0.0-create-tables.xml b/351004/Brazhalovich/Lab_1/src/main/resources/db/changelog/changeset/v1.0.0-create-tables.xml new file mode 100644 index 000000000..1e9a7eb5b --- /dev/null +++ b/351004/Brazhalovich/Lab_1/src/main/resources/db/changelog/changeset/v1.0.0-create-tables.xml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SELECT setval('user_seq', 1); + + + \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/resources/db/changelog/db.changelog-master.xml b/351004/Brazhalovich/Lab_1/src/main/resources/db/changelog/db.changelog-master.xml new file mode 100644 index 000000000..aad6f19d0 --- /dev/null +++ b/351004/Brazhalovich/Lab_1/src/main/resources/db/changelog/db.changelog-master.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/test/java/org/example/newsapi/AbstractIntegrationTest.java b/351004/Brazhalovich/Lab_1/src/test/java/org/example/newsapi/AbstractIntegrationTest.java new file mode 100644 index 000000000..08c8379b2 --- /dev/null +++ b/351004/Brazhalovich/Lab_1/src/test/java/org/example/newsapi/AbstractIntegrationTest.java @@ -0,0 +1,41 @@ +package org.example.newsapi; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@Testcontainers +public abstract class AbstractIntegrationTest { + + // Определяем контейнер PostgreSQL (версия 15-alpine) + @Container + static PostgreSQLContainer postgres = new PostgreSQLContainer<>("postgres:15-alpine") + .withDatabaseName("distcomp") + .withUsername("postgres") + .withPassword("postgres"); + + // Динамически подменяем настройки application.properties на настройки контейнера + @DynamicPropertySource + static void configureProperties(DynamicPropertyRegistry registry) { + registry.add("spring.datasource.url", () -> postgres.getJdbcUrl() + "?currentSchema=distcomp"); + registry.add("spring.datasource.username", postgres::getUsername); + registry.add("spring.datasource.password", postgres::getPassword); + + // Указываем Hibernate и Liquibase использовать схему по умолчанию public (так проще в тестах) + // или ту, что мы создали. Если скрипт Liquibase требует схему distcomp, + // нам нужно убедиться, что она создана. + // Но TestContainers создает пустую БД. + // Hibernate валидирует схему. + // Проще всего переопределить схему на public для тестов, + // либо добавить инициализирующий скрипт для создания схемы. + + // В данном случае мы используем URL с параметром currentSchema=distcomp. + // Postgres в тестконтейнере может не иметь этой схемы. + // Поэтому добавим команду на создание схемы при старте контейнера: + postgres.withInitScript("db/test-init-schema.sql"); + } +} \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/test/java/org/example/newsapi/controller/NewsControllerTest.java b/351004/Brazhalovich/Lab_1/src/test/java/org/example/newsapi/controller/NewsControllerTest.java new file mode 100644 index 000000000..4e83c7fbb --- /dev/null +++ b/351004/Brazhalovich/Lab_1/src/test/java/org/example/newsapi/controller/NewsControllerTest.java @@ -0,0 +1,135 @@ +package org.example.newsapi.controller; + +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import org.example.newsapi.AbstractIntegrationTest; +import org.example.newsapi.dto.request.NewsRequestTo; +import org.example.newsapi.entity.News; +import org.example.newsapi.entity.User; +import org.example.newsapi.repository.NewsRepository; +import org.example.newsapi.repository.UserRepository; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.web.server.LocalServerPort; + +import java.util.List; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.*; + +class NewsControllerTest extends AbstractIntegrationTest { + + @LocalServerPort + private int port; + + @Autowired + private NewsRepository newsRepository; + + @Autowired + private UserRepository userRepository; + + @BeforeEach + void setUp() { + RestAssured.port = port; + RestAssured.basePath = "/api/v1.0/news"; + } + + @AfterEach + void tearDown() { + // Очищаем новости после каждого теста, чтобы не влиять на другие тесты + newsRepository.deleteAll(); + } + + @Test + void shouldCreateNews() { + // ID=1 создается автоматически скриптом Liquibase (sashabrazhalovich2005@gmail.com) + Long userId = 1L; + + NewsRequestTo request = new NewsRequestTo(); + request.setUserId(userId); + request.setTitle("Breaking News"); + request.setContent("Something happened today."); + + given() + .contentType(ContentType.JSON) + .body(request) + .when() + .post() + .then() + .statusCode(201) + .body("id", notNullValue()) + .body("title", equalTo("Breaking News")) + .body("userId", equalTo(userId.intValue())); + } + + @Test + void shouldGetAllNewsWithPagination() { + // Подготовка данных напрямую через репозиторий + User user = userRepository.findById(1L).orElseThrow(); + + News news1 = News.builder().user(user).title("Title 1").content("Content 1").build(); + News news2 = News.builder().user(user).title("Title 2").content("Content 2").build(); + newsRepository.saveAll(List.of(news1, news2)); + + // Проверка GET запроса с пагинацией + given() + .param("page", 0) + .param("size", 10) + .param("sort", "id,asc") + .when() + .get() + .then() + .statusCode(200) + .body("content", hasSize(2)) + .body("content[0].title", equalTo("Title 1")) + .body("totalElements", equalTo(2)); + } + + @Test + void shouldUpdateNews() { + // Создаем новость + User user = userRepository.findById(1L).orElseThrow(); + News news = News.builder().user(user).title("Old Title").content("Old Content").build(); + news = newsRepository.save(news); + + // Формируем запрос на обновление + NewsRequestTo updateRequest = new NewsRequestTo(); + updateRequest.setUserId(user.getId()); // Автор остается тот же + updateRequest.setTitle("New Title"); + updateRequest.setContent("New Content"); + + given() + .contentType(ContentType.JSON) + .body(updateRequest) + .when() + .put("/{id}", news.getId()) + .then() + .statusCode(200) + .body("title", equalTo("New Title")) + .body("content", equalTo("New Content")); + } + + @Test + void shouldDeleteNews() { + // Создаем новость + User user = userRepository.findById(1L).orElseThrow(); + News news = News.builder().user(user).title("To Delete").content("...").build(); + news = newsRepository.save(news); + + // Удаляем + given() + .when() + .delete("/{id}", news.getId()) + .then() + .statusCode(204); + + // Проверяем, что удалилась (ожидаем 404 при попытке получить) + given() + .when() + .get("/{id}", news.getId()) + .then() + .statusCode(404); + } +} \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/test/resources/db/test-init-schema.sql b/351004/Brazhalovich/Lab_1/src/test/resources/db/test-init-schema.sql new file mode 100644 index 000000000..f10ef46c6 --- /dev/null +++ b/351004/Brazhalovich/Lab_1/src/test/resources/db/test-init-schema.sql @@ -0,0 +1 @@ +CREATE SCHEMA IF NOT EXISTS distcomp; \ No newline at end of file From b2c95e800f0fc0663e1be4baa3c2cc465aaf34d9 Mon Sep 17 00:00:00 2001 From: sasha Date: Tue, 24 Feb 2026 23:50:07 +0300 Subject: [PATCH 4/7] jpa without "Jdbc Check Marker (Wait 3 Markers in News with id=20)" --- .../newsapi/dto/request/CommentRequestTo.java | 4 ++++ .../newsapi/dto/request/MarkerRequestTo.java | 6 ++--- .../newsapi/dto/request/NewsRequestTo.java | 10 ++++++++ .../dto/response/CommentResponseTo.java | 4 ++++ .../newsapi/dto/response/NewsResponseTo.java | 9 +++++++ .../newsapi/repository/MarkerRepository.java | 2 +- .../newsapi/repository/NewsRepository.java | 1 - .../newsapi/service/MarkerService.java | 8 +++++-- .../example/newsapi/service/NewsService.java | 24 +++++++++++-------- 9 files changed, 50 insertions(+), 18 deletions(-) diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/request/CommentRequestTo.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/request/CommentRequestTo.java index d61b71f22..a5f5d73f1 100644 --- a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/request/CommentRequestTo.java +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/request/CommentRequestTo.java @@ -14,4 +14,8 @@ public class CommentRequestTo { @Size(min = 2, max = 2048) private String content; + + public void setNews(Long news) { + this.newsId = news; + } } \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/request/MarkerRequestTo.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/request/MarkerRequestTo.java index 6f3e24e02..5512a3815 100644 --- a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/request/MarkerRequestTo.java +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/request/MarkerRequestTo.java @@ -7,10 +7,8 @@ import lombok.NoArgsConstructor; @Data -@NoArgsConstructor // ОБЯЗАТЕЛЬНО -@AllArgsConstructor public class MarkerRequestTo { - @NotBlank - @Size(min = 2, max = 32) +// @NotBlank +// @Size(min = 2, max = 32) private String name; } \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/request/NewsRequestTo.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/request/NewsRequestTo.java index e95328739..97b1c1ab9 100644 --- a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/request/NewsRequestTo.java +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/request/NewsRequestTo.java @@ -20,4 +20,14 @@ public class NewsRequestTo { //@JsonProperty("marker") private Set markerIds; + + // Джексон увидит этот метод и положит данные из JSON-поля "user" в твой "userId" + public void setUser(Long user) { + this.userId = user; + } + + // Джексон увидит этот метод и положит данные из JSON-поля "marker" в твой "markerIds" + public void setMarker(java.util.Set marker) { + this.markerIds = marker; + } } \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/response/CommentResponseTo.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/response/CommentResponseTo.java index b5aa9de31..a91991fbf 100644 --- a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/response/CommentResponseTo.java +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/response/CommentResponseTo.java @@ -12,4 +12,8 @@ public class CommentResponseTo { private Long newsId; private String content; + + public Long getNews() { + return this.newsId; + } } \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/response/NewsResponseTo.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/response/NewsResponseTo.java index 6ef64d7bf..d9ca497b1 100644 --- a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/response/NewsResponseTo.java +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/response/NewsResponseTo.java @@ -23,4 +23,13 @@ public class NewsResponseTo { //@JsonProperty("marker") private Set markerIds = new HashSet<>(); + + public Long getUser() { + return this.userId; + } + + // Jackson создаст поле "marker" в JSON ответе + public Set getMarker() { + return this.markerIds; + } } \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/MarkerRepository.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/MarkerRepository.java index c16187183..c225d8262 100644 --- a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/MarkerRepository.java +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/MarkerRepository.java @@ -6,5 +6,5 @@ @Repository public interface MarkerRepository extends JpaRepository { - boolean existsByName(String name); + boolean existsByName(String name); // Добавьте этот метод } \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/NewsRepository.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/NewsRepository.java index 1494b1ba9..09f44a852 100644 --- a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/NewsRepository.java +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/NewsRepository.java @@ -7,6 +7,5 @@ @Repository public interface NewsRepository extends JpaRepository, JpaSpecificationExecutor { - // Добавь этот метод: boolean existsByTitle(String title); } \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/service/MarkerService.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/service/MarkerService.java index 2d2e4d229..45468a2ae 100644 --- a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/service/MarkerService.java +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/service/MarkerService.java @@ -4,6 +4,7 @@ import org.example.newsapi.dto.request.MarkerRequestTo; import org.example.newsapi.dto.response.MarkerResponseTo; import org.example.newsapi.entity.Marker; +import org.example.newsapi.exception.AlreadyExistsException; import org.example.newsapi.exception.NotFoundException; import org.example.newsapi.mapper.MarkerMapper; import org.example.newsapi.repository.MarkerRepository; @@ -22,10 +23,13 @@ public class MarkerService { @Transactional public MarkerResponseTo create(MarkerRequestTo request) { + // 1. ПРОВЕРКА НА ДУБЛИКАТ: Если маркер с таким именем уже есть, тестер ждет 403. + if (markerRepository.existsByName(request.getName())) { + throw new AlreadyExistsException("Marker already exists: " + request.getName()); + } + Marker marker = markerMapper.toEntity(request); Marker saved = markerRepository.save(marker); - // Добавь лог, чтобы видеть в консоли, что маркер реально сохранился - System.out.println("Saved marker: " + saved.getName() + " with ID: " + saved.getId()); return markerMapper.toDto(saved); } diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/service/NewsService.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/service/NewsService.java index 45fe62358..dbef586ca 100644 --- a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/service/NewsService.java +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/service/NewsService.java @@ -33,26 +33,29 @@ public class NewsService { @Transactional public NewsResponseTo create(NewsRequestTo request) { - // Если userId null (т.е. тестер не прислал поле "user" или прислал "userId") + // 1. Проверка юзера (для возврата 403, если его нет) if (request.getUserId() == null || !userRepository.existsById(request.getUserId())) { - throw new NotFoundException("User not found or missing"); + throw new org.example.newsapi.exception.NotFoundException("User not found"); } - // Проверка уникальности заголовка (важно!) + // 2. Проверка уникальности заголовка (предотвращает дубликаты) if (newsRepository.existsByTitle(request.getTitle())) { - throw new AlreadyExistsException("News title already exists"); + throw new org.example.newsapi.exception.AlreadyExistsException("Title exists"); } - User user = userRepository.getReferenceById(request.getUserId()); News news = newsMapper.toEntity(request); - news.setUser(user); + news.setUser(userRepository.getReferenceById(request.getUserId())); - // Принудительно ставим даты для ответа - news.setCreated(java.time.LocalDateTime.now()); - news.setModified(java.time.LocalDateTime.now()); + // Принудительно ставим время + java.time.LocalDateTime now = java.time.LocalDateTime.now(); + news.setCreated(now); + news.setModified(now); + // 3. ПРИВЯЗКА МАРКЕРОВ + // Если наши "умные" сеттеры сработали, markerIds не будет пустым if (request.getMarkerIds() != null && !request.getMarkerIds().isEmpty()) { - java.util.List markers = markerRepository.findAllById(request.getMarkerIds()); + java.util.List markers = + markerRepository.findAllById(request.getMarkerIds()); news.setMarkers(new java.util.HashSet<>(markers)); } @@ -60,6 +63,7 @@ public NewsResponseTo create(NewsRequestTo request) { return newsMapper.toDto(saved); } + public Page findAll(Pageable pageable) { return newsRepository.findAll(pageable).map(newsMapper::toDto); } From ba9dec03b93c055ec4696f1c1dc20f81556804c1 Mon Sep 17 00:00:00 2001 From: sasha Date: Wed, 25 Feb 2026 01:01:37 +0300 Subject: [PATCH 5/7] Start fixing global --- .../newsapi/dto/request/MarkerRequestTo.java | 6 ++- .../newsapi/dto/request/NewsRequestTo.java | 1 + .../exception/GlobalExceptionHandler.java | 48 ++++++++----------- .../newsapi/service/MarkerService.java | 9 +++- .../example/newsapi/service/NewsService.java | 28 ++++++----- .../changeset/v1.0.0-create-tables.xml | 12 +++-- 6 files changed, 55 insertions(+), 49 deletions(-) diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/request/MarkerRequestTo.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/request/MarkerRequestTo.java index 5512a3815..63db5845b 100644 --- a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/request/MarkerRequestTo.java +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/request/MarkerRequestTo.java @@ -7,8 +7,10 @@ import lombok.NoArgsConstructor; @Data +@NoArgsConstructor // Обязательно для правильной работы Jackson +@AllArgsConstructor public class MarkerRequestTo { -// @NotBlank -// @Size(min = 2, max = 32) + @NotBlank + @Size(min = 2, max = 32) private String name; } \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/request/NewsRequestTo.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/request/NewsRequestTo.java index 97b1c1ab9..d83f0650f 100644 --- a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/request/NewsRequestTo.java +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/request/NewsRequestTo.java @@ -28,6 +28,7 @@ public void setUser(Long user) { // Джексон увидит этот метод и положит данные из JSON-поля "marker" в твой "markerIds" public void setMarker(java.util.Set marker) { + System.out.println(">>> RECEIVED MARKER IDs FOR NEWS: " + marker); this.markerIds = marker; } } \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/exception/GlobalExceptionHandler.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/exception/GlobalExceptionHandler.java index 18b39c852..4fe2b3df8 100644 --- a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/exception/GlobalExceptionHandler.java +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/exception/GlobalExceptionHandler.java @@ -6,42 +6,34 @@ import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; -import jakarta.validation.ConstraintViolationException; @RestControllerAdvice public class GlobalExceptionHandler { - // 1. Ошибки "Не найдено" (User, News и т.д.) -> Статус 403, Код 40301 - @ExceptionHandler(NotFoundException.class) - public ResponseEntity handleNotFound(NotFoundException e) { - return ResponseEntity.status(HttpStatus.FORBIDDEN) - .body(new ErrorResponse(e.getMessage(), 40301)); - } + // 1. БИЗНЕС-ОШИБКИ (Не найдено или Дубликат логина/заголовка) -> 403 + // Объединяем их в один метод, чтобы не было конфликтов - // 2. Ошибки дубликатов (логин занят) -> Статус 403, Код 40301 - @ExceptionHandler(AlreadyExistsException.class) - public ResponseEntity handleAlreadyExists(AlreadyExistsException e) { - return ResponseEntity.status(HttpStatus.FORBIDDEN) - .body(new ErrorResponse(e.getMessage(), 40301)); - } + @ExceptionHandler({NotFoundException.class, AlreadyExistsException.class}) + public ResponseEntity handleBusinessError(RuntimeException e) { + // Обязательно FORBIDDEN (403) и 40301 + return ResponseEntity.status(HttpStatus.FORBIDDEN) + .body(new ErrorResponse(e.getMessage(), 40301)); + } - // 3. Ошибки базы данных (те самые SQL Error: 23505 и 22001 из твоего лога) - @ExceptionHandler(org.springframework.dao.DataIntegrityViolationException.class) - public ResponseEntity handleDataIntegrity(Exception e) { - // Любая ошибка БД (например, попытка создать новость с несуществующим userId, - // если проверка в сервисе вдруг пропустила) -> 403 - return ResponseEntity.status(HttpStatus.FORBIDDEN) - .body(new ErrorResponse("Integrity violation", 40301)); - } + @ExceptionHandler(org.springframework.dao.DataIntegrityViolationException.class) + public ResponseEntity handleSqlError(Exception e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN) + .body(new ErrorResponse("Database error", 40301)); + } + + @ExceptionHandler(org.springframework.web.bind.MethodArgumentNotValidException.class) + public ResponseEntity handleValidationError(Exception e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN) + .body(new ErrorResponse("Validation error", 40301)); + } - // 4. Ошибки валидации (аннотации @Size, @NotBlank) -> Статус 403, Код 40301 - @ExceptionHandler({MethodArgumentNotValidException.class, ConstraintViolationException.class}) - public ResponseEntity handleValidation(Exception e) { - return ResponseEntity.status(HttpStatus.FORBIDDEN) - .body(new ErrorResponse("Validation error", 40301)); - } - // 5. Все остальные непредвиденные ошибки -> 500 + // 4. ВСЕ ОСТАЛЬНЫЕ ОШИБКИ -> 500 @ExceptionHandler(Exception.class) public ResponseEntity handleAll(Exception e) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/service/MarkerService.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/service/MarkerService.java index 45468a2ae..c30ee4453 100644 --- a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/service/MarkerService.java +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/service/MarkerService.java @@ -23,13 +23,18 @@ public class MarkerService { @Transactional public MarkerResponseTo create(MarkerRequestTo request) { - // 1. ПРОВЕРКА НА ДУБЛИКАТ: Если маркер с таким именем уже есть, тестер ждет 403. + System.out.println(">>> ATTEMPTING TO CREATE MARKER WITH NAME: " + request.getName()); + if (markerRepository.existsByName(request.getName())) { - throw new AlreadyExistsException("Marker already exists: " + request.getName()); + System.out.println(">>> MARKER ALREADY EXISTS: " + request.getName()); + throw new AlreadyExistsException("Marker already exists"); } Marker marker = markerMapper.toEntity(request); Marker saved = markerRepository.save(marker); + + System.out.println(">>> SUCCESSFULLY SAVED MARKER: " + saved.getName() + " WITH ID: " + saved.getId()); + return markerMapper.toDto(saved); } diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/service/NewsService.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/service/NewsService.java index dbef586ca..01c99912d 100644 --- a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/service/NewsService.java +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/service/NewsService.java @@ -33,29 +33,31 @@ public class NewsService { @Transactional public NewsResponseTo create(NewsRequestTo request) { - // 1. Проверка юзера (для возврата 403, если его нет) + // 1. Проверка юзера (Если его нет - бросаем ошибку) if (request.getUserId() == null || !userRepository.existsById(request.getUserId())) { - throw new org.example.newsapi.exception.NotFoundException("User not found"); + throw new NotFoundException("User not found"); } - // 2. Проверка уникальности заголовка (предотвращает дубликаты) + // 2. Проверка дубликата заголовка (ИМЕННО ЭТО ВАЛИЛО ТЕСТ №9) if (newsRepository.existsByTitle(request.getTitle())) { - throw new org.example.newsapi.exception.AlreadyExistsException("Title exists"); + throw new AlreadyExistsException("News title already exists"); } + User user = userRepository.getReferenceById(request.getUserId()); News news = newsMapper.toEntity(request); - news.setUser(userRepository.getReferenceById(request.getUserId())); + news.setUser(user); - // Принудительно ставим время - java.time.LocalDateTime now = java.time.LocalDateTime.now(); - news.setCreated(now); - news.setModified(now); + // Ставим даты + news.setCreated(java.time.LocalDateTime.now()); + news.setModified(java.time.LocalDateTime.now()); - // 3. ПРИВЯЗКА МАРКЕРОВ - // Если наши "умные" сеттеры сработали, markerIds не будет пустым + // 3. Обработка маркеров (Проверяем, что ID маркеров реально есть в базе) if (request.getMarkerIds() != null && !request.getMarkerIds().isEmpty()) { - java.util.List markers = - markerRepository.findAllById(request.getMarkerIds()); + java.util.List markers = markerRepository.findAllById(request.getMarkerIds()); + // Если тестер прислал несуществующий маркер, можно тоже выкинуть 403 + if (markers.size() != request.getMarkerIds().size()) { + throw new NotFoundException("One or more markers not found"); + } news.setMarkers(new java.util.HashSet<>(markers)); } diff --git a/351004/Brazhalovich/Lab_1/src/main/resources/db/changelog/changeset/v1.0.0-create-tables.xml b/351004/Brazhalovich/Lab_1/src/main/resources/db/changelog/changeset/v1.0.0-create-tables.xml index 1e9a7eb5b..4e93eb694 100644 --- a/351004/Brazhalovich/Lab_1/src/main/resources/db/changelog/changeset/v1.0.0-create-tables.xml +++ b/351004/Brazhalovich/Lab_1/src/main/resources/db/changelog/changeset/v1.0.0-create-tables.xml @@ -37,7 +37,8 @@ - + + @@ -70,7 +71,8 @@ - + + @@ -82,10 +84,12 @@ - + + - + + From 7f17aff5b3d212b48cc5617ad077c8a9ee16625b Mon Sep 17 00:00:00 2001 From: sasha Date: Wed, 25 Feb 2026 02:04:27 +0300 Subject: [PATCH 6/7] Start deepseek --- .../java/org/example/newsapi/entity/News.java | 27 +++--- .../exception/GlobalExceptionHandler.java | 12 +-- .../example/newsapi/mapper/NewsMapper.java | 1 + .../example/newsapi/service/NewsService.java | 22 ++--- .../changeset/v1.0.0-create-tables.xml | 97 +++++++------------ 5 files changed, 64 insertions(+), 95 deletions(-) diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/entity/News.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/entity/News.java index a7507d720..a2d5228f8 100644 --- a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/entity/News.java +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/entity/News.java @@ -4,6 +4,8 @@ import lombok.*; import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.UpdateTimestamp; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; import java.time.LocalDateTime; import java.util.HashSet; @@ -23,35 +25,34 @@ public class News { @SequenceGenerator(name = "news_seq_gen", sequenceName = "news_seq", allocationSize = 1) private Long id; - // Связь многие-к-одному с User @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id", nullable = false) - @ToString.Exclude + // ВАЖНО: это удалит новость, когда удаляется юзер + @org.hibernate.annotations.OnDelete(action = org.hibernate.annotations.OnDeleteAction.CASCADE) private User user; - @Column(nullable = false, length = 64) + @Column(nullable = false, unique = true, length = 64) private String title; @Column(nullable = false, length = 2048) private String content; - @CreationTimestamp // Автоматически ставит дату при создании + @CreationTimestamp private LocalDateTime created; - @UpdateTimestamp // Автоматически обновляет дату при изменении + @UpdateTimestamp private LocalDateTime modified; - // Связь один-ко-многим с комментариями - @OneToMany(mappedBy = "news", cascade = CascadeType.ALL, orphanRemoval = true) - @ToString.Exclude - private List comments; - - // Связь многие-ко-многим с маркерами через промежуточную таблицу - @ManyToMany(fetch = FetchType.EAGER) // Поставь EAGER для тестов, чтобы маппер всегда видел маркеры + @Builder.Default + @ManyToMany(fetch = FetchType.EAGER) @JoinTable( - name = "tbl_news_marker", + name = "tbl_news_marker", // ТЕПЕРЬ ТАБЛИЦА БУДЕТ БЕЗ "S" joinColumns = @JoinColumn(name = "news_id"), inverseJoinColumns = @JoinColumn(name = "marker_id") ) private Set markers = new HashSet<>(); + + @OneToMany(mappedBy = "news", cascade = CascadeType.ALL, orphanRemoval = true) + @ToString.Exclude + private List comments; } \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/exception/GlobalExceptionHandler.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/exception/GlobalExceptionHandler.java index 4fe2b3df8..180763074 100644 --- a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/exception/GlobalExceptionHandler.java +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/exception/GlobalExceptionHandler.java @@ -13,12 +13,12 @@ public class GlobalExceptionHandler { // 1. БИЗНЕС-ОШИБКИ (Не найдено или Дубликат логина/заголовка) -> 403 // Объединяем их в один метод, чтобы не было конфликтов - @ExceptionHandler({NotFoundException.class, AlreadyExistsException.class}) - public ResponseEntity handleBusinessError(RuntimeException e) { - // Обязательно FORBIDDEN (403) и 40301 - return ResponseEntity.status(HttpStatus.FORBIDDEN) - .body(new ErrorResponse(e.getMessage(), 40301)); - } + @ExceptionHandler({NotFoundException.class, AlreadyExistsException.class}) + public ResponseEntity handleBusinessError(RuntimeException e) { + System.out.println(">>> HANDLED BUSINESS ERROR: " + e.getMessage()); // ЛОГ ДЛЯ НАС + return ResponseEntity.status(HttpStatus.FORBIDDEN) + .body(new ErrorResponse(e.getMessage(), 40301)); + } @ExceptionHandler(org.springframework.dao.DataIntegrityViolationException.class) public ResponseEntity handleSqlError(Exception e) { diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/mapper/NewsMapper.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/mapper/NewsMapper.java index 62d3e4608..5be19a1c5 100644 --- a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/mapper/NewsMapper.java +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/mapper/NewsMapper.java @@ -22,6 +22,7 @@ public interface NewsMapper { @Mapping(target = "comments", ignore = true) News toEntity(NewsRequestTo request); + @Mapping(target = "marker", ignore = true) // Чтобы MapStruct не искал это поле @Mapping(target = "userId", source = "user.id") @Mapping(target = "markerIds", expression = "java(news.getMarkers() != null ? news.getMarkers().stream().map(Marker::getId).collect(Collectors.toSet()) : new java.util.HashSet<>())") NewsResponseTo toDto(News news); diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/service/NewsService.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/service/NewsService.java index 01c99912d..96c092e71 100644 --- a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/service/NewsService.java +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/service/NewsService.java @@ -33,12 +33,10 @@ public class NewsService { @Transactional public NewsResponseTo create(NewsRequestTo request) { - // 1. Проверка юзера (Если его нет - бросаем ошибку) if (request.getUserId() == null || !userRepository.existsById(request.getUserId())) { throw new NotFoundException("User not found"); } - // 2. Проверка дубликата заголовка (ИМЕННО ЭТО ВАЛИЛО ТЕСТ №9) if (newsRepository.existsByTitle(request.getTitle())) { throw new AlreadyExistsException("News title already exists"); } @@ -46,26 +44,18 @@ public NewsResponseTo create(NewsRequestTo request) { User user = userRepository.getReferenceById(request.getUserId()); News news = newsMapper.toEntity(request); news.setUser(user); + news.setCreated(LocalDateTime.now()); + news.setModified(LocalDateTime.now()); - // Ставим даты - news.setCreated(java.time.LocalDateTime.now()); - news.setModified(java.time.LocalDateTime.now()); - - // 3. Обработка маркеров (Проверяем, что ID маркеров реально есть в базе) if (request.getMarkerIds() != null && !request.getMarkerIds().isEmpty()) { - java.util.List markers = markerRepository.findAllById(request.getMarkerIds()); - // Если тестер прислал несуществующий маркер, можно тоже выкинуть 403 - if (markers.size() != request.getMarkerIds().size()) { - throw new NotFoundException("One or more markers not found"); - } - news.setMarkers(new java.util.HashSet<>(markers)); + List markers = markerRepository.findAllById(request.getMarkerIds()); + news.setMarkers(new HashSet<>(markers)); } - News saved = newsRepository.save(news); + // ИСПОЛЬЗУЕМ saveAndFlush чтобы данные мгновенно попали в БД + News saved = newsRepository.saveAndFlush(news); return newsMapper.toDto(saved); } - - public Page findAll(Pageable pageable) { return newsRepository.findAll(pageable).map(newsMapper::toDto); } diff --git a/351004/Brazhalovich/Lab_1/src/main/resources/db/changelog/changeset/v1.0.0-create-tables.xml b/351004/Brazhalovich/Lab_1/src/main/resources/db/changelog/changeset/v1.0.0-create-tables.xml index 4e93eb694..a07b8a8cd 100644 --- a/351004/Brazhalovich/Lab_1/src/main/resources/db/changelog/changeset/v1.0.0-create-tables.xml +++ b/351004/Brazhalovich/Lab_1/src/main/resources/db/changelog/changeset/v1.0.0-create-tables.xml @@ -5,7 +5,6 @@ xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.24.xsd"> - @@ -13,99 +12,77 @@ - - - - - - - - - - - - + + + + - - - - - - - - - - - - - - - - - + + + + + + - - - - - - - + + - - - - - - - - - - - + + + + - - - - - - - - - + + + + - - + - SELECT setval('user_seq', 1); + + + + + + + + + + + + + + + SELECT setval('marker_seq', 3); + \ No newline at end of file From efca00068112bda29e4295e8bd1119b678fa713f Mon Sep 17 00:00:00 2001 From: sasha Date: Wed, 25 Feb 2026 02:22:09 +0300 Subject: [PATCH 7/7] JPA --- .../newsapi/controller/NewsController.java | 2 + .../newsapi/dto/request/NewsRequestTo.java | 20 ++++---- .../java/org/example/newsapi/entity/News.java | 4 +- .../newsapi/repository/MarkerRepository.java | 5 +- .../example/newsapi/service/NewsService.java | 46 +++++++++++++++---- 5 files changed, 56 insertions(+), 21 deletions(-) diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/controller/NewsController.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/controller/NewsController.java index 829ff1f34..33b5aa61c 100644 --- a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/controller/NewsController.java +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/controller/NewsController.java @@ -23,6 +23,8 @@ public class NewsController { @PostMapping @ResponseStatus(HttpStatus.CREATED) public NewsResponseTo create(@RequestBody @Valid NewsRequestTo request) { + System.out.println(">>> CREATE NEWS REQUEST: " + request); + System.out.println(">>> markerNames: " + request.getMarkerNames()); return newsService.create(request); } diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/request/NewsRequestTo.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/request/NewsRequestTo.java index d83f0650f..eca5f34f4 100644 --- a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/request/NewsRequestTo.java +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/request/NewsRequestTo.java @@ -8,7 +8,6 @@ @Data public class NewsRequestTo { - //@JsonProperty("user") // Тестер будет слать "user", а Jackson запишет это в userId @NotNull private Long userId; @@ -18,17 +17,18 @@ public class NewsRequestTo { @Size(min = 4, max = 2048) private String content; - //@JsonProperty("marker") - private Set markerIds; + // Это поле будет заполняться через сеттеры ниже + private Set markerNames; - // Джексон увидит этот метод и положит данные из JSON-поля "user" в твой "userId" - public void setUser(Long user) { - this.userId = user; + // Сеттер для JSON-поля "marker" + public void setMarker(Set markerNames) { + System.out.println(">>> setMarker called with: " + markerNames); + this.markerNames = markerNames; } - // Джексон увидит этот метод и положит данные из JSON-поля "marker" в твой "markerIds" - public void setMarker(java.util.Set marker) { - System.out.println(">>> RECEIVED MARKER IDs FOR NEWS: " + marker); - this.markerIds = marker; + // Сеттер для JSON-поля "markers" (на случай множественного числа) + public void setMarkers(Set markerNames) { + System.out.println(">>> setMarkers called with: " + markerNames); + this.markerNames = markerNames; } } \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/entity/News.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/entity/News.java index a2d5228f8..db3334af3 100644 --- a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/entity/News.java +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/entity/News.java @@ -44,9 +44,9 @@ public class News { private LocalDateTime modified; @Builder.Default - @ManyToMany(fetch = FetchType.EAGER) + @ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL) @JoinTable( - name = "tbl_news_marker", // ТЕПЕРЬ ТАБЛИЦА БУДЕТ БЕЗ "S" + name = "tbl_news_marker", joinColumns = @JoinColumn(name = "news_id"), inverseJoinColumns = @JoinColumn(name = "marker_id") ) diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/MarkerRepository.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/MarkerRepository.java index c225d8262..3a67b8755 100644 --- a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/MarkerRepository.java +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/MarkerRepository.java @@ -4,7 +4,10 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.Optional; + @Repository public interface MarkerRepository extends JpaRepository { - boolean existsByName(String name); // Добавьте этот метод + boolean existsByName(String name); + Optional findByName(String name); // добавить этот метод } \ No newline at end of file diff --git a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/service/NewsService.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/service/NewsService.java index 96c092e71..be1f84afc 100644 --- a/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/service/NewsService.java +++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/service/NewsService.java @@ -20,6 +20,7 @@ import java.time.LocalDateTime; import java.util.HashSet; import java.util.List; +import java.util.Set; @Service @RequiredArgsConstructor @@ -33,29 +34,43 @@ public class NewsService { @Transactional public NewsResponseTo create(NewsRequestTo request) { + // 1. Проверяем пользователя if (request.getUserId() == null || !userRepository.existsById(request.getUserId())) { throw new NotFoundException("User not found"); } + // 2. Проверяем уникальность заголовка if (newsRepository.existsByTitle(request.getTitle())) { throw new AlreadyExistsException("News title already exists"); } + // 3. Создаём новость User user = userRepository.getReferenceById(request.getUserId()); News news = newsMapper.toEntity(request); news.setUser(user); news.setCreated(LocalDateTime.now()); news.setModified(LocalDateTime.now()); - if (request.getMarkerIds() != null && !request.getMarkerIds().isEmpty()) { - List markers = markerRepository.findAllById(request.getMarkerIds()); - news.setMarkers(new HashSet<>(markers)); + // 4. Обрабатываем маркеры по именам + if (request.getMarkerNames() != null && !request.getMarkerNames().isEmpty()) { + Set markers = new HashSet<>(); + for (String name : request.getMarkerNames()) { + Marker marker = markerRepository.findByName(name) + .orElseGet(() -> { + // Создаём новый маркер, если не найден + Marker newMarker = Marker.builder().name(name).build(); + return markerRepository.save(newMarker); + }); + markers.add(marker); + } + news.setMarkers(markers); } - // ИСПОЛЬЗУЕМ saveAndFlush чтобы данные мгновенно попали в БД News saved = newsRepository.saveAndFlush(news); return newsMapper.toDto(saved); } + + public Page findAll(Pageable pageable) { return newsRepository.findAll(pageable).map(newsMapper::toDto); } @@ -71,22 +86,37 @@ public NewsResponseTo update(Long id, NewsRequestTo request) { News news = newsRepository.findById(id) .orElseThrow(() -> new NotFoundException("News not found")); + // Проверяем, что пользователь существует if (!userRepository.existsById(request.getUserId())) { throw new NotFoundException("User not found"); } + // Обновляем поля новости (кроме маркеров) newsMapper.updateEntityFromDto(request, news); news.setUser(userRepository.getReferenceById(request.getUserId())); news.setModified(LocalDateTime.now()); - if (request.getMarkerIds() != null) { - List markers = markerRepository.findAllById(request.getMarkerIds()); - news.setMarkers(new HashSet<>(markers)); + // Обрабатываем маркеры по именам + if (request.getMarkerNames() != null) { + Set markers = new HashSet<>(); + for (String name : request.getMarkerNames()) { + Marker marker = markerRepository.findByName(name) + .orElseGet(() -> { + // Создаём новый маркер, если не найден + Marker newMarker = Marker.builder().name(name).build(); + return markerRepository.save(newMarker); + }); + markers.add(marker); + } + news.setMarkers(markers); + } else { + // Если список имён не передан, можно оставить маркеры без изменений + // или очистить связь — зависит от требований + // news.setMarkers(new HashSet<>()); } return newsMapper.toDto(newsRepository.save(news)); } - @Transactional public void delete(Long id) { if (!newsRepository.existsById(id)) {