diff --git a/351004/Brazhalovich/Lab_1/pom.xml b/351004/Brazhalovich/Lab_1/pom.xml
new file mode 100644
index 000000000..aa3c8eba7
--- /dev/null
+++ b/351004/Brazhalovich/Lab_1/pom.xml
@@ -0,0 +1,140 @@
+
+
+ 4.0.0
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.2.2
+
+
+
+ org.example
+ newsapi
+ 1.0-SNAPSHOT
+ newsapi
+ REST API with JPA and Liquibase
+
+
+ 21
+ 1.5.5.Final
+ 1.18.30
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+
+
+ org.postgresql
+ postgresql
+ runtime
+
+
+
+
+ org.liquibase
+ liquibase-core
+
+
+
+
+ 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-testcontainers
+ test
+
+
+ org.testcontainers
+ junit-jupiter
+ test
+
+
+ org.testcontainers
+ postgresql
+ 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..7e740fae1
--- /dev/null
+++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/controller/CommentController.java
@@ -0,0 +1,49 @@
+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.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.*;
+
+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(@PageableDefault(size = 50) Pageable pageable) {
+ return commentService.findAll(pageable).getContent();
+ }
+
+ @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);
+ }
+}
\ 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..107d89e87
--- /dev/null
+++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/controller/MarkerController.java
@@ -0,0 +1,49 @@
+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.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.*;
+
+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) { // ПРОВЕРЬ @Valid
+ return markerService.create(request);
+ }
+
+ @GetMapping
+ public List getAll(@PageableDefault(size = 50) Pageable pageable) {
+ return markerService.findAll(pageable).getContent();
+ }
+
+ @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..33b5aa61c
--- /dev/null
+++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/controller/NewsController.java
@@ -0,0 +1,51 @@
+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.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.*;
+
+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) {
+ System.out.println(">>> CREATE NEWS REQUEST: " + request);
+ System.out.println(">>> markerNames: " + request.getMarkerNames());
+ return newsService.create(request);
+ }
+
+ @GetMapping
+ public List getAll(@PageableDefault(size = 50) Pageable pageable) {
+ return newsService.findAll(pageable).getContent();
+ }
+
+ @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..e2ea47fae
--- /dev/null
+++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/controller/UserController.java
@@ -0,0 +1,50 @@
+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 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.*;
+
+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(@PageableDefault(size = 50) Pageable pageable) {
+ // Возвращаем только контент списка, чтобы тестер не путался в мета-данных Page
+ return userService.findAll(pageable).getContent();
+ }
+
+ @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..a5f5d73f1
--- /dev/null
+++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/request/CommentRequestTo.java
@@ -0,0 +1,21 @@
+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;
+
+ @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
new file mode 100644
index 000000000..63db5845b
--- /dev/null
+++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/request/MarkerRequestTo.java
@@ -0,0 +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 // Обязательно для правильной работы Jackson
+@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
new file mode 100644
index 000000000..eca5f34f4
--- /dev/null
+++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/request/NewsRequestTo.java
@@ -0,0 +1,34 @@
+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;
+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 markerNames;
+
+ // Сеттер для JSON-поля "marker"
+ public void setMarker(Set markerNames) {
+ System.out.println(">>> setMarker called with: " + markerNames);
+ this.markerNames = markerNames;
+ }
+
+ // Сеттер для 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/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..a91991fbf
--- /dev/null
+++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/response/CommentResponseTo.java
@@ -0,0 +1,19 @@
+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;
+
+ 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/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..d9ca497b1
--- /dev/null
+++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/response/NewsResponseTo.java
@@ -0,0 +1,35 @@
+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;
+
+ //@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/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..164b19ed7
--- /dev/null
+++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/dto/response/UserResponseTo.java
@@ -0,0 +1,15 @@
+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;
+ 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/Comment.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/entity/Comment.java
new file mode 100644
index 000000000..27dfab0f6
--- /dev/null
+++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/entity/Comment.java
@@ -0,0 +1,26 @@
+package org.example.newsapi.entity;
+
+import jakarta.persistence.*;
+import lombok.*;
+
+@Entity
+@Table(name = "tbl_comment")
+@Data
+@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
new file mode 100644
index 000000000..e61bc7936
--- /dev/null
+++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/entity/Marker.java
@@ -0,0 +1,21 @@
+package org.example.newsapi.entity;
+
+import jakarta.persistence.*;
+import lombok.*;
+
+@Entity
+@Table(name = "tbl_marker")
+@Data
+@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
new file mode 100644
index 000000000..db3334af3
--- /dev/null
+++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/entity/News.java
@@ -0,0 +1,58 @@
+package org.example.newsapi.entity;
+
+import jakarta.persistence.*;
+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;
+import java.util.List;
+import java.util.Set;
+
+@Entity
+@Table(name = "tbl_news")
+@Data
+@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;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "user_id", nullable = false)
+ // ВАЖНО: это удалит новость, когда удаляется юзер
+ @org.hibernate.annotations.OnDelete(action = org.hibernate.annotations.OnDeleteAction.CASCADE)
+ private User user;
+
+ @Column(nullable = false, unique = true, length = 64)
+ private String title;
+
+ @Column(nullable = false, length = 2048)
+ private String content;
+
+ @CreationTimestamp
+ private LocalDateTime created;
+
+ @UpdateTimestamp
+ private LocalDateTime modified;
+
+ @Builder.Default
+ @ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
+ @JoinTable(
+ name = "tbl_news_marker",
+ 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/entity/User.java b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/entity/User.java
new file mode 100644
index 000000000..b95952017
--- /dev/null
+++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/entity/User.java
@@ -0,0 +1,38 @@
+package org.example.newsapi.entity;
+
+import jakarta.persistence.*;
+import lombok.*;
+
+import java.util.List;
+
+@Entity
+@Table(name = "tbl_user")
+@Data
+@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/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/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..180763074
--- /dev/null
+++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/exception/GlobalExceptionHandler.java
@@ -0,0 +1,42 @@
+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;
+
+@RestControllerAdvice
+public class GlobalExceptionHandler {
+
+ // 1. БИЗНЕС-ОШИБКИ (Не найдено или Дубликат логина/заголовка) -> 403
+ // Объединяем их в один метод, чтобы не было конфликтов
+
+ @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) {
+ 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. ВСЕ ОСТАЛЬНЫЕ ОШИБКИ -> 500
+ @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..ba3e2255f
--- /dev/null
+++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/mapper/CommentMapper.java
@@ -0,0 +1,23 @@
+package org.example.newsapi.mapper;
+
+import org.example.newsapi.dto.request.CommentRequestTo;
+import org.example.newsapi.dto.response.CommentResponseTo;
+import org.example.newsapi.entity.Comment;
+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
new file mode 100644
index 000000000..ea739545d
--- /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.response.MarkerResponseTo;
+import org.example.newsapi.entity.Marker;
+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)
+ 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..5be19a1c5
--- /dev/null
+++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/mapper/NewsMapper.java
@@ -0,0 +1,37 @@
+package org.example.newsapi.mapper;
+
+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;
+
+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 = "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);
+
+ @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
new file mode 100644
index 000000000..e3572b9c8
--- /dev/null
+++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/mapper/UserMapper.java
@@ -0,0 +1,22 @@
+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 {
+
+ @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/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..3a67b8755
--- /dev/null
+++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/MarkerRepository.java
@@ -0,0 +1,13 @@
+package org.example.newsapi.repository;
+
+import org.example.newsapi.entity.Marker;
+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);
+ Optional findByName(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..09f44a852
--- /dev/null
+++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/repository/NewsRepository.java
@@ -0,0 +1,11 @@
+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/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..3aa80985a
--- /dev/null
+++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/service/CommentService.java
@@ -0,0 +1,73 @@
+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.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 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 Page findAll(Pageable pageable) {
+ return commentRepository.findAll(pageable)
+ .map(commentMapper::toDto);
+ }
+
+ public CommentResponseTo findById(Long id) {
+ return commentRepository.findById(id)
+ .map(commentMapper::toDto)
+ .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);
+
+ // Если меняется привязка к новости (редкий кейс, но возможный)
+ 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.existsById(id)) {
+ throw new NotFoundException("Comment not found with id: " + id);
+ }
+ commentRepository.deleteById(id);
+ }
+}
\ 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..c30ee4453
--- /dev/null
+++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/service/MarkerService.java
@@ -0,0 +1,68 @@
+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.AlreadyExistsException;
+import org.example.newsapi.exception.NotFoundException;
+import org.example.newsapi.mapper.MarkerMapper;
+import org.example.newsapi.repository.MarkerRepository;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Service;
+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) {
+ System.out.println(">>> ATTEMPTING TO CREATE MARKER WITH NAME: " + request.getName());
+
+ if (markerRepository.existsByName(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);
+ }
+
+ public Page findAll(Pageable pageable) {
+ return markerRepository.findAll(pageable)
+ .map(markerMapper::toDto);
+ }
+
+ public MarkerResponseTo findById(Long id) {
+ return markerRepository.findById(id)
+ .map(markerMapper::toDto)
+ .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.save(marker));
+ }
+
+ @Transactional
+ public void delete(Long id) {
+ if (!markerRepository.existsById(id)) {
+ 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..be1f84afc
--- /dev/null
+++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/service/NewsService.java
@@ -0,0 +1,127 @@
+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.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.Set;
+
+@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) {
+ // 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());
+
+ // 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);
+ }
+
+ News saved = newsRepository.saveAndFlush(news);
+ return newsMapper.toDto(saved);
+ }
+
+
+ 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"));
+ }
+
+ @Transactional
+ 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.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)) {
+ throw new NotFoundException("News not found");
+ }
+ 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..daa805e7a
--- /dev/null
+++ b/351004/Brazhalovich/Lab_1/src/main/java/org/example/newsapi/service/UserService.java
@@ -0,0 +1,60 @@
+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.UserRepository;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Service;
+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 Page findAll(Pageable pageable) {
+ return userRepository.findAll(pageable)
+ .map(userMapper::toDto);
+ }
+
+ public UserResponseTo findById(Long id) {
+ return userRepository.findById(id)
+ .map(userMapper::toDto)
+ .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.save(user));
+ }
+
+ @Transactional
+ public void delete(Long id) {
+ if (!userRepository.existsById(id)) {
+ 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..473056974
--- /dev/null
+++ b/351004/Brazhalovich/Lab_1/src/main/resources/application.properties
@@ -0,0 +1,11 @@
+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
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..a07b8a8cd
--- /dev/null
+++ b/351004/Brazhalovich/Lab_1/src/main/resources/db/changelog/changeset/v1.0.0-create-tables.xml
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ SELECT setval('user_seq', 1);
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ SELECT setval('marker_seq', 3);
+
+
\ 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