diff --git a/pom.xml b/pom.xml index a973f82..b68f756 100644 --- a/pom.xml +++ b/pom.xml @@ -61,6 +61,27 @@ 4.0.0 test + + + + com.h2database + h2 + runtime + + + + + org.springframework.boot + spring-boot-starter-jdbc + + + + + org.springframework.boot + spring-boot-starter-test + test + + diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java index ef0a166..b0a6080 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java @@ -1,73 +1,118 @@ package ru.yandex.practicum.filmorate.controller; +import com.fasterxml.jackson.databind.node.ObjectNode; import jakarta.validation.Valid; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import org.springframework.beans.factory.annotation.Autowired; -import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.model.Buffer; import ru.yandex.practicum.filmorate.model.Film; -import ru.yandex.practicum.filmorate.service.FilmService; +import ru.yandex.practicum.filmorate.model.FilmResponse; +import ru.yandex.practicum.filmorate.service.FilmInterface; +import ru.yandex.practicum.filmorate.storage.film.FilmStorage; +import ru.yandex.practicum.filmorate.storage.user.UserStorage; -import java.util.Collection; -import java.util.Map; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.*; @RestController @RequestMapping("/films") public class FilmController { - private final FilmService filmService; + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + private static final String DEFAULT_GENRE = "нет жанра"; + + private final FilmStorage filmStorage; + private final UserStorage userStorage; + private final FilmInterface filmInterface; @Autowired - public FilmController(FilmService filmService) { - this.filmService = filmService; + public FilmController( + FilmStorage filmStorage, + UserStorage userStorage, + FilmInterface filmInterface + ) { + this.filmStorage = filmStorage; + this.userStorage = userStorage; + this.filmInterface = filmInterface; } - //создание фильма - @PostMapping - public Film createFilm(@Valid @RequestBody Film film) { - return filmService.createFilm(film); + @GetMapping + public List findAll() { + return filmStorage.findAll(); } - //обновление фильма - @PutMapping - public Film update(@Valid @RequestBody Film film) { - return filmService.update(film); + @GetMapping("/{id}") + public FilmResponse findById(@PathVariable("id") Long id) { + return filmStorage.findById(id); } - //получение всех фильмов - @GetMapping - public Collection getFilms() { - return filmService.getFilms(); + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public FilmResponse create(@Valid @RequestBody ObjectNode objectNode) { + Buffer buffer = parseObjectNodeToBuffer(objectNode); + return filmStorage.create(buffer); } - //получение фильма по ID - @GetMapping("/{id}") - public Film getFilmById(@PathVariable Long id) { - return filmService.getFilmById(id); + @PutMapping + public FilmResponse update(@Valid @RequestBody ObjectNode objectNode) { + Buffer buffer = parseObjectNodeToBuffer(objectNode); + return filmStorage.update(buffer); } @PutMapping("/{id}/like/{userId}") - public ResponseEntity addLike(@PathVariable Long id, @PathVariable Long userId) { - try { - filmService.addLike(id, userId); - return ResponseEntity.ok().build(); // Возвращаем 200 OK, если лайк добавлен - } catch (NotFoundException e) { - return ResponseEntity.status(HttpStatus.NOT_FOUND) - .body(Map.of("error", e.getMessage())); // Возвращаем 404 и JSON с ошибкой - } + public FilmResponse addLike(@Valid @PathVariable("id") Long id, @PathVariable("userId") Long userId) { + return filmInterface.addLike(userId, id); } - //удаление лайка из фильма @DeleteMapping("/{id}/like/{userId}") - public ResponseEntity removeLike(@PathVariable Long id, @PathVariable Long userId) { - filmService.removeLike(id, userId); - return ResponseEntity.ok().build(); + public FilmResponse delLike(@Valid @PathVariable("id") Long id, @PathVariable("userId") Long userId) { + return filmInterface.delLike(userId, id); } - //получение популярных фильмов @GetMapping("/popular") - public Collection getTopFilms(@RequestParam(defaultValue = "10") int count) { - return filmService.getTopFilms(count); + public LinkedHashSet viewRating(@RequestParam(defaultValue = "10") Long count) { + return filmInterface.viewRating(count); + } + + /** + * преобразует json объект в объект Buffer + * + * @param objectNode json объект + * @return объект Buffer + */ + private Buffer parseObjectNodeToBuffer(ObjectNode objectNode) { + Long id = objectNode.has("id") ? objectNode.get("id").asLong() : 0L; + String name = objectNode.get("name").asText(); + String description = objectNode.get("description").asText(); + String releaseDate = objectNode.get("releaseDate").asText(); + Integer duration = objectNode.get("duration").asInt(); + List mpa = objectNode.get("mpa").findValuesAsText("id"); + List genres = extractGenresFromObjectNode(objectNode); + + return Buffer.of( + id, + name, + description, + LocalDate.parse(releaseDate, DATE_FORMATTER), + duration, + genres, + Long.valueOf(mpa.get(0)) + ); + } + + /** + * извлекает список жанров из json объекта + * + * @param objectNode json объект + * @return список жанров + */ + private List extractGenresFromObjectNode(ObjectNode objectNode) { + try { + return objectNode.get("genres").findValuesAsText("id"); + } catch (NullPointerException e) { + return List.of(DEFAULT_GENRE); + } } } \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/GenreController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/GenreController.java new file mode 100644 index 0000000..5d1ed35 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/GenreController.java @@ -0,0 +1,37 @@ +package ru.yandex.practicum.filmorate.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import ru.yandex.practicum.filmorate.model.Genre; +import ru.yandex.practicum.filmorate.service.GenreService; + +import java.util.List; + +@RestController +@RequestMapping("/genres") +@RequiredArgsConstructor +public class GenreController { + + private final GenreService genreService; + + /** + * получить жанр по его идентификатору + * + * @param id идентификатор жанра + * @return объект жанра + */ + @GetMapping("/{id}") + public Genre getGenreById(@PathVariable Long id) { + return genreService.getGenreById(id); + } + + /** + * получить список всех жанров + * + * @return список всех жанров + */ + @GetMapping + public List getAllGenres() { + return genreService.getAllGenres(); + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/MpaController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/MpaController.java new file mode 100644 index 0000000..6561c16 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/MpaController.java @@ -0,0 +1,37 @@ +package ru.yandex.practicum.filmorate.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import ru.yandex.practicum.filmorate.model.Mpa; +import ru.yandex.practicum.filmorate.service.MpaService; + +import java.util.List; + +@RestController +@RequestMapping("/mpa") +@RequiredArgsConstructor +public class MpaController { + + private final MpaService mpaService; + + /** + * получить рейтинг mpa по его идентификатору + * + * @param id идентификатор рейтинга mpa + * @return объект рейтинга mpa + */ + @GetMapping("/{id}") + public Mpa getMpaById(@PathVariable Long id) { + return mpaService.getMpaById(id); + } + + /** + * получить список всех рейтингов mpa + * + * @return список всех рейтингов mpa + */ + @GetMapping + public List getAllMpa() { + return mpaService.getAllMpa(); + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java index 7cdbea6..707d71a 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java @@ -1,68 +1,117 @@ package ru.yandex.practicum.filmorate.controller; import jakarta.validation.Valid; -import org.springframework.beans.factory.annotation.Autowired; +import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; -import ru.yandex.practicum.filmorate.exception.DuplicatedDataException; -import ru.yandex.practicum.filmorate.exception.NotFoundException; -import ru.yandex.practicum.filmorate.exception.ValidationException; import ru.yandex.practicum.filmorate.model.User; -import ru.yandex.practicum.filmorate.service.UserService; +import ru.yandex.practicum.filmorate.service.UserInterface; +import ru.yandex.practicum.filmorate.storage.user.UserStorage; import java.util.Collection; +import java.util.Set; @RestController @RequestMapping("/users") +@RequiredArgsConstructor public class UserController { - private final UserService userService; + private static final String USER_ID_PATH = "/{id}"; + private static final String FRIENDS_PATH = USER_ID_PATH + "/friends"; + private static final String FRIEND_ID_PATH = FRIENDS_PATH + "/{friendId}"; + private static final String COMMON_FRIENDS_PATH = USER_ID_PATH + "/friends/common/{otherId}"; - @Autowired - public UserController(UserService userService) { - this.userService = userService; - } + private final UserStorage userStorage; + private final UserInterface userInterface; + /** + * получить список всех пользователей + * + * @return список всех пользователей + */ @GetMapping public Collection findAll() { - return userService.findAll(); + return userStorage.findAll(); + } + + /** + * получить пользователя по его ид + * + * @param id идентификатор пользователя + * @return объект пользователя + */ + @GetMapping(USER_ID_PATH) + public User findById(@PathVariable("id") Long id) { + return userStorage.findById(id); } + /** + * создать нового пользователя + * + * @param user объект пользователя + * @return созданный пользователь + */ @PostMapping @ResponseStatus(HttpStatus.CREATED) - public User create(@Valid @RequestBody User user) throws ValidationException, DuplicatedDataException { - return userService.create(user); + public User create(@Valid @RequestBody User user) { + return userStorage.create(user); } + /** + * обновить данные пользователя + * + * @param newUser объект пользователя с новыми данными + * @return обновленный пользователь + */ @PutMapping - public User update(@Valid @RequestBody User user) throws NotFoundException, ValidationException { - return userService.update(user); - } - - @GetMapping("/{id}") - public User getUserById(@PathVariable Long id) throws NotFoundException, ValidationException { - return userService.findById(id); + public User update(@Valid @RequestBody User newUser) { + return userStorage.update(newUser); } - @PutMapping("/{id}/friends/{friendId}") - @ResponseStatus(HttpStatus.NO_CONTENT) - public void addFriend(@PathVariable Long id, @PathVariable Long friendId) throws NotFoundException { - userService.addFriend(id, friendId); + /** + * добавить друга пользователю + * + * @param id идентификатор пользователя + * @param friendId идентификатор друга + * @return пользователь с обновленным списком друзей + */ + @PutMapping(FRIEND_ID_PATH) + public User addFriend(@Valid @PathVariable("id") Long id, @PathVariable("friendId") Long friendId) { + return userInterface.addFriend(id, friendId); } - @DeleteMapping("/{id}/friends/{friendId}") - @ResponseStatus(HttpStatus.NO_CONTENT) - public void removeFriend(@PathVariable Long id, @PathVariable Long friendId) throws NotFoundException { - userService.removeFriend(id, friendId); + /** + * удалить друга у пользователя + * + * @param id идентификатор пользователя + * @param friendId идентификатор друга + * @return пользователь с обновленным списком друзей + */ + @DeleteMapping(FRIEND_ID_PATH) + public User delFriend(@Valid @PathVariable("id") Long id, @PathVariable("friendId") Long friendId) { + return userInterface.delFriend(id, friendId); } - @GetMapping("/{id}/friends") - public Collection getFriends(@PathVariable Long id) throws NotFoundException { - return userService.getFriends(id); + /** + * получить список общих друзей двух пользователей + * + * @param id идентификатор первого пользователя + * @param otherId идентификатор второго пользователя + * @return список общих друзей + */ + @GetMapping(COMMON_FRIENDS_PATH) + public Set findJointFriends(@Valid @PathVariable("id") Long id, @PathVariable("otherId") Long otherId) { + return userInterface.findJointFriends(id, otherId); } - @GetMapping("/{id}/friends/common/{otherId}") - public Collection getCommonFriends(@PathVariable Long id, @PathVariable Long otherId) throws NotFoundException { - return userService.getCommonFriends(id, otherId); + /** + * получить список друзей пользователя + * + * @param id идентификатор пользователя + * @return список друзей + */ + @GetMapping(FRIENDS_PATH) + public Set findAllFriends(@Valid @PathVariable("id") Long id) { + return userInterface.findAllFriends(id); } } \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/exception/ConditionsNotMetException.java b/src/main/java/ru/yandex/practicum/filmorate/exception/ConditionsNotMetException.java new file mode 100644 index 0000000..d84fada --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/exception/ConditionsNotMetException.java @@ -0,0 +1,9 @@ +package ru.yandex.practicum.filmorate.exception; + +import java.util.Objects; + +public class ConditionsNotMetException extends RuntimeException { + public ConditionsNotMetException(String message) { + super(Objects.requireNonNull(message, "Сообщение об ошибке не может быть null")); + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/exception/ErrorHandler.java b/src/main/java/ru/yandex/practicum/filmorate/exception/ErrorHandler.java index e03903a..8afe7ff 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/exception/ErrorHandler.java +++ b/src/main/java/ru/yandex/practicum/filmorate/exception/ErrorHandler.java @@ -46,4 +46,10 @@ public ResponseEntity> handleMethodArgumentNotValidException return ResponseEntity.status(HttpStatus.BAD_REQUEST) .body(Map.of("error", message)); } + + @ExceptionHandler(ConditionsNotMetException.class) + public ResponseEntity> handleConditionsNotMetException(ConditionsNotMetException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(Map.of("error", e.getMessage())); + } } \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/exception/ExceptionMessages.java b/src/main/java/ru/yandex/practicum/filmorate/exception/ExceptionMessages.java deleted file mode 100644 index af7a70f..0000000 --- a/src/main/java/ru/yandex/practicum/filmorate/exception/ExceptionMessages.java +++ /dev/null @@ -1,11 +0,0 @@ -package ru.yandex.practicum.filmorate.exception; - -public class ExceptionMessages { - - public static final String FILM_ID_CANNOT_BE_NULL = "Film ID cannot be null"; - public static final String FILM_NOT_FOUND = "Film with id %d not found"; - public static final String FILM_NAME_CANNOT_BE_EMPTY = "Film name cannot be empty"; - public static final String FILM_DURATION_INVALID = "Film duration must be a positive number"; - public static final String FILM_DESCRIPTION_TOO_LONG = "Film description cannot exceed 200 characters"; - public static final String FILM_RELEASE_DATE_INVALID = "Film release date cannot be earlier than December 28, 1895"; -} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/Buffer.java b/src/main/java/ru/yandex/practicum/filmorate/model/Buffer.java new file mode 100644 index 0000000..97ce14b --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Buffer.java @@ -0,0 +1,41 @@ +package ru.yandex.practicum.filmorate.model; + +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jdk.jfr.Description; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; + +import java.time.LocalDate; +import java.util.*; + +@Data +@Builder +@AllArgsConstructor(staticName = "of") +public class Buffer { + private Long id; + @NotNull + @NotBlank + private String name; + @Description("New film update description") + private String description; + @JsonFormat(pattern = "yyyy-MM-dd") + private LocalDate releaseDate; + @NotNull + private Integer duration; + private List genres; + private Long mpa; + + public Map toMapBuffer() { + Map values = new HashMap<>(); + values.put("id", id); + values.put("name", name); + values.put("description", description); + values.put("releaseDate", releaseDate); + values.put("duration", duration); + values.put("ratingId", mpa); + return values; + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/Film.java b/src/main/java/ru/yandex/practicum/filmorate/model/Film.java index cf96ab1..df78ac8 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/Film.java +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Film.java @@ -1,34 +1,49 @@ package ru.yandex.practicum.filmorate.model; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Positive; -import jakarta.validation.constraints.Size; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.validation.constraints.*; +import jdk.jfr.Description; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; import java.time.LocalDate; -import java.util.HashSet; -import java.util.Set; +import java.util.*; -import lombok.Getter; -import lombok.Setter; - -@Getter -@Setter +@Data +@Builder +@EqualsAndHashCode(of = {"id"}) +@AllArgsConstructor(staticName = "of") public class Film { private Long id; - @NotBlank(message = "Название фильма не может быть пустым") + @NotNull(message = "Название не может быть null") + @NotBlank(message = "Название не может быть пустым") + @Size(max = 100, message = "Название не может быть длиннее 100 символов") private String name; - @Size(max = 200, message = "Описание фильма не может превышать 200 символов") + @Description("New film update description") + @Size(max = 200, message = "Максимальная длина описания — 200 символов") private String description; - @NotNull(message = "Дата релиза фильма не может быть null") + @NotNull(message = "Дата релиза не может быть null") + @JsonFormat(pattern = "yyyy-MM-dd") + @PastOrPresent(message = "Дата релиза должна быть не раньше 28 декабря 1895 года") private LocalDate releaseDate; - @NotNull(message = "Продолжительность фильма должна быть указана") - @Positive(message = "Film duration must be a positive number") - private int duration; + @NotNull(message = "Продолжительность не может быть null") + @Positive(message = "Продолжительность фильма должна быть положительным числом") + private Integer duration; + + @JsonIgnore + private Set likedUsers; + + @NotNull(message = "Рейтинг MPA не может быть null") + @Min(value = 1, message = "Рейтинг MPA должен быть не меньше 1") + @Max(value = 5, message = "Рейтинг MPA должен быть не больше 5") + private Long mpa; - private Set likedUsers = new HashSet<>(); + private LinkedHashSet genres; } \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/FilmResponse.java b/src/main/java/ru/yandex/practicum/filmorate/model/FilmResponse.java new file mode 100644 index 0000000..1c02cd9 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/FilmResponse.java @@ -0,0 +1,35 @@ +package ru.yandex.practicum.filmorate.model; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jdk.jfr.Description; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDate; +import java.util.*; + +@Data +@Builder +@EqualsAndHashCode(of = {"id"}) +@AllArgsConstructor(staticName = "of") +public class FilmResponse { + private Long id; + @NotNull + @NotBlank + private String name; + @Description("New film update description") + private String description; + @JsonFormat(pattern = "yyyy-MM-dd") + private LocalDate releaseDate; + @NotNull + private Integer duration; + @JsonIgnore + private Set likedUsers; + private Mpa mpa; + private LinkedHashSet genres; +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/FriendRequest.java b/src/main/java/ru/yandex/practicum/filmorate/model/FriendRequest.java new file mode 100644 index 0000000..e713a4d --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/FriendRequest.java @@ -0,0 +1,15 @@ +package ru.yandex.practicum.filmorate.model; + +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.RequiredArgsConstructor; + +@Data +@RequiredArgsConstructor(staticName = "of") +public class FriendRequest { + @NotNull + private Long id; + private Long userId; + private Long friendId; + private boolean accept = false; +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/Genre.java b/src/main/java/ru/yandex/practicum/filmorate/model/Genre.java new file mode 100644 index 0000000..d3914eb --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Genre.java @@ -0,0 +1,13 @@ +package ru.yandex.practicum.filmorate.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(of = {"id"}) +@AllArgsConstructor(staticName = "of") +public class Genre { + private Long id; + private String name; +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/GenreConstant.java b/src/main/java/ru/yandex/practicum/filmorate/model/GenreConstant.java new file mode 100644 index 0000000..0c0d9ca --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/GenreConstant.java @@ -0,0 +1,13 @@ +package ru.yandex.practicum.filmorate.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(of = {"id"}) +@AllArgsConstructor(staticName = "of") +public class GenreConstant { + private Long id; + private String name; +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/Mpa.java b/src/main/java/ru/yandex/practicum/filmorate/model/Mpa.java new file mode 100644 index 0000000..35e715a --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Mpa.java @@ -0,0 +1,13 @@ +package ru.yandex.practicum.filmorate.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(of = {"id"}) +@AllArgsConstructor(staticName = "of") +public class Mpa { + private Long id; + private String name; +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/User.java b/src/main/java/ru/yandex/practicum/filmorate/model/User.java index 2cdf9e4..0bec4f5 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/User.java +++ b/src/main/java/ru/yandex/practicum/filmorate/model/User.java @@ -1,40 +1,42 @@ package ru.yandex.practicum.filmorate.model; import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.EqualsAndHashCode; +import lombok.*; import java.time.LocalDate; -import java.util.HashSet; +import java.util.HashMap; +import java.util.Map; import java.util.Set; @Data +@Builder @EqualsAndHashCode(of = {"email"}) @AllArgsConstructor(staticName = "of") public class User { private Long id; private String name; - @Email(message = "Invalid email") - @NotBlank(message = "Email cannot be empty") + @Email private String email; - @NotNull(message = "Login cannot be null") - @NotBlank(message = "Login cannot be empty or contain spaces") + @NotNull + @NotBlank private String login; @JsonFormat(pattern = "yyyy-MM-dd") - @NotNull(message = "Birthday cannot be null") private LocalDate birthday; - private Set friends; + @JsonIgnore + private Set friendRequests; - public Set getFriends() { - return friends == null ? new HashSet<>() : friends; // Защита от null - } - - public void setFriends(Set friends) { - this.friends = friends == null ? new HashSet<>() : friends; // Защита от null + public Map toMapUser() { + Map values = new HashMap<>(); + values.put("id", id); + values.put("name", name); + values.put("email", email); + values.put("login", login); + values.put("birthday", birthday); + return values; } } \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/FilmInterface.java b/src/main/java/ru/yandex/practicum/filmorate/service/FilmInterface.java new file mode 100644 index 0000000..ab6de1f --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/FilmInterface.java @@ -0,0 +1,13 @@ +package ru.yandex.practicum.filmorate.service; + +import ru.yandex.practicum.filmorate.model.FilmResponse; + +import java.util.LinkedHashSet; + +public interface FilmInterface { + FilmResponse addLike(Long idUser, Long idFilm); + + FilmResponse delLike(Long idUser, Long idFilm); + + LinkedHashSet viewRating(Long count); +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java b/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java index d1ffbec..083fc31 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java @@ -1,52 +1,96 @@ package ru.yandex.practicum.filmorate.service; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.exception.ConditionsNotMetException; import ru.yandex.practicum.filmorate.exception.NotFoundException; -import ru.yandex.practicum.filmorate.exception.ValidationException; -import ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.model.FilmResponse; +import ru.yandex.practicum.filmorate.storage.film.FilmDbStorage; import ru.yandex.practicum.filmorate.storage.film.FilmStorage; +import ru.yandex.practicum.filmorate.storage.user.UserStorage; -import java.util.Collection; -import java.util.List; +import java.util.*; @Service -@Slf4j -public class FilmService { +@Slf4j(topic = "TRACE") +@RequiredArgsConstructor +public class FilmService implements FilmInterface { + private final UserStorage userStorage; private final FilmStorage filmStorage; + private final JdbcTemplate jdbcTemplate; - @Autowired - public FilmService(FilmStorage filmStorage) { - this.filmStorage = filmStorage; - } - - public Collection getFilms() { - return filmStorage.findAll(); - } - - public Film createFilm(Film film) throws ValidationException { - return filmStorage.create(film); - } - - public Film update(Film film) throws NotFoundException, ValidationException { - return filmStorage.update(film); - } - - public Film getFilmById(Long id) throws NotFoundException { - return filmStorage.findById(id); - } + // SQL-запросы + private final String selectLikedUsersQuery = "select filmId, userId from likedUsers"; + private final String insertLikeQuery = "insert into likedUsers(filmId, userId) values (?, ?)"; + private final String selectFilmGenresQuery = "select filmId, genreId from filmGenre where filmId = ?"; + private final String deleteLikeQuery = "delete from likedUsers where filmId = ? and userId = ?"; + private final String selectTopFilmsQuery = "select f.id as name, COUNT(l.userId) as coun from likedUsers as l LEFT OUTER JOIN film AS f ON l.filmId = f.id GROUP BY f.name ORDER BY COUNT(l.userId) DESC LIMIT 10"; - public void addLike(Long filmId, Long userId) throws NotFoundException { - filmStorage.addLike(filmId, userId); + @Override + public FilmResponse addLike(Long idUser, Long idFilm) { + log.info("Обработка Post-запроса..."); + if (userStorage.findById(idUser) != null && filmStorage.findById(idFilm) != null) { + Map> likedUsers = jdbcTemplate.query(selectLikedUsersQuery, new FilmDbStorage.LikedUsersExtractor()); + if (likedUsers.get(idFilm) != null && likedUsers.get(idFilm).contains(idUser)) { + log.error("Пользователь с ID {} уже поставил лайк фильму с ID {}", idUser, idFilm); + throw new ConditionsNotMetException("Пользователь с ID " + idUser + " уже поставил лайк фильму с ID " + idFilm); + } else { + jdbcTemplate.update(insertLikeQuery, idFilm, idUser); + } + } + FilmResponse film = filmStorage.findById(idFilm); + LinkedHashSet genres = new LinkedHashSet<>(); + Map> filmGenre = jdbcTemplate.query(selectFilmGenresQuery, new FilmDbStorage.FilmGenreExtractor(), film.getId()); + if (!filmGenre.isEmpty()) { + for (Long g : filmGenre.get(film.getId())) + genres.add(g); + } + return FilmResponse.of(film.getId(), film.getName(), film.getDescription(), film.getReleaseDate(), film.getDuration(), new HashSet<>(), film.getMpa(), genres); } - public void removeLike(Long filmId, Long userId) throws NotFoundException { - filmStorage.removeLike(filmId, userId); + @Override + public FilmResponse delLike(Long idUser, Long idFilm) { + log.info("Обработка Del-запроса..."); + if (userStorage.findById(idUser) != null && filmStorage.findById(idFilm) != null) { + Map> likedUsers = jdbcTemplate.query(selectLikedUsersQuery, new FilmDbStorage.LikedUsersExtractor()); + if (likedUsers.get(idFilm) != null && !likedUsers.get(idFilm).contains(idUser)) { + log.error("Пользователь с ID {} не ставил лайк фильму с ID {}", idUser, idFilm); + throw new ConditionsNotMetException("Пользователь с ID " + idUser + " не ставил лайк фильму с ID " + idFilm); + } else { + jdbcTemplate.update(deleteLikeQuery, idFilm, idUser); + } + } + FilmResponse film = filmStorage.findById(idFilm); + LinkedHashSet genres = new LinkedHashSet<>(); + Map> filmGenre = jdbcTemplate.query(selectFilmGenresQuery, new FilmDbStorage.FilmGenreExtractor(), film.getId()); + if (!filmGenre.isEmpty()) { + for (Long g : filmGenre.get(film.getId())) + genres.add(g); + } + return FilmResponse.of(film.getId(), film.getName(), film.getDescription(), film.getReleaseDate(), film.getDuration(), new HashSet<>(), film.getMpa(), genres); } - public List getTopFilms(int count) { - return filmStorage.getTopFilms(count); + public LinkedHashSet viewRating(Long count) { + log.info("Обработка Get-запроса..."); + LinkedHashMap likedUsers = jdbcTemplate.query(selectTopFilmsQuery, new TopLikedUsersExtractor()); + LinkedHashSet films = new LinkedHashSet<>(); + if (likedUsers == null) { + log.error("Список фильмов с рейтингом пуст."); + throw new NotFoundException("Список фильмов с рейтингом пуст."); + } else { + LinkedHashSet genres = new LinkedHashSet<>(); + for (Long l : likedUsers.keySet()) { + Map> filmGenre = jdbcTemplate.query(selectFilmGenresQuery, new FilmDbStorage.FilmGenreExtractor(), filmStorage.findById(l).getId()); + if (!filmGenre.isEmpty()) { + for (Long g : filmGenre.get(filmStorage.findById(l).getId())) + genres.add(g); + } + films.add(FilmResponse.of(filmStorage.findById(l).getId(), filmStorage.findById(l).getName(), filmStorage.findById(l).getDescription(), filmStorage.findById(l).getReleaseDate(), filmStorage.findById(l).getDuration(), new HashSet<>(), filmStorage.findById(l).getMpa(), genres)); + } + } + return films; } } \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/GenreExtractor.java b/src/main/java/ru/yandex/practicum/filmorate/service/GenreExtractor.java new file mode 100644 index 0000000..2805ca6 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/GenreExtractor.java @@ -0,0 +1,21 @@ +package ru.yandex.practicum.filmorate.service; + +import org.springframework.jdbc.core.ResultSetExtractor; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.LinkedHashMap; +import java.util.Map; + +public class GenreExtractor implements ResultSetExtractor> { + @Override + public Map extractData(ResultSet rs) throws SQLException { + Map data = new LinkedHashMap<>(); + while (rs.next()) { + Long id = rs.getLong("id"); + String name = rs.getString("name"); + data.put(id, name); + } + return data; + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/GenreService.java b/src/main/java/ru/yandex/practicum/filmorate/service/GenreService.java new file mode 100644 index 0000000..3c35f6e --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/GenreService.java @@ -0,0 +1,27 @@ +package ru.yandex.practicum.filmorate.service; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.model.Genre; +import ru.yandex.practicum.filmorate.storage.genre.GenreDbStorage; + +import java.util.List; + +@Service +public class GenreService { + + private final GenreDbStorage genreStorage; + + @Autowired + public GenreService(GenreDbStorage genreStorage) { + this.genreStorage = genreStorage; + } + + public Genre getGenreById(Long id) { + return genreStorage.getGenreById(id); + } + + public List getAllGenres() { + return genreStorage.getAllGenres(); + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/MpaService.java b/src/main/java/ru/yandex/practicum/filmorate/service/MpaService.java new file mode 100644 index 0000000..fa8d754 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/MpaService.java @@ -0,0 +1,27 @@ +package ru.yandex.practicum.filmorate.service; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.model.Mpa; +import ru.yandex.practicum.filmorate.storage.mpa.MpaDbStorage; + +import java.util.List; + +@Service +public class MpaService { + + private final MpaDbStorage mpaStorage; + + @Autowired + public MpaService(MpaDbStorage mpaStorage) { + this.mpaStorage = mpaStorage; + } + + public Mpa getMpaById(Long id) { + return mpaStorage.getMpaById(id); + } + + public List getAllMpa() { + return mpaStorage.getAllMpa(); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/RatingNameExtractor.java b/src/main/java/ru/yandex/practicum/filmorate/service/RatingNameExtractor.java new file mode 100644 index 0000000..3e37198 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/RatingNameExtractor.java @@ -0,0 +1,21 @@ +package ru.yandex.practicum.filmorate.service; + +import org.springframework.jdbc.core.ResultSetExtractor; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.Map; + +public class RatingNameExtractor implements ResultSetExtractor> { + @Override + public Map extractData(ResultSet rs) throws SQLException { + Map data = new HashMap<>(); + while (rs.next()) { + Long id = rs.getLong("id"); + String rating = rs.getString("rating"); + data.put(id, rating); + } + return data; + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/TopLikedUsersExtractor.java b/src/main/java/ru/yandex/practicum/filmorate/service/TopLikedUsersExtractor.java new file mode 100644 index 0000000..b2d79a7 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/TopLikedUsersExtractor.java @@ -0,0 +1,20 @@ +package ru.yandex.practicum.filmorate.service; + +import org.springframework.jdbc.core.ResultSetExtractor; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.LinkedHashMap; + +public class TopLikedUsersExtractor implements ResultSetExtractor> { + @Override + public LinkedHashMap extractData(ResultSet rs) throws SQLException { + LinkedHashMap data = new LinkedHashMap<>(); + while (rs.next()) { + Long filmId = rs.getLong("name"); + Long likes = rs.getLong("coun"); + data.putIfAbsent(filmId, likes); + } + return data; + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/UserInterface.java b/src/main/java/ru/yandex/practicum/filmorate/service/UserInterface.java new file mode 100644 index 0000000..648199e --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/UserInterface.java @@ -0,0 +1,15 @@ +package ru.yandex.practicum.filmorate.service; + +import ru.yandex.practicum.filmorate.model.User; + +import java.util.Set; + +public interface UserInterface { + User addFriend(Long idUser, Long idFriend); + + User delFriend(Long idUser, Long idFriend); + + Set findJointFriends(Long idUser, Long idFriend); + + Set findAllFriends(Long idUser); +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java b/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java index 197a046..ae96394 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java @@ -1,56 +1,123 @@ package ru.yandex.practicum.filmorate.service; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; -import ru.yandex.practicum.filmorate.exception.DuplicatedDataException; +import ru.yandex.practicum.filmorate.exception.ConditionsNotMetException; import ru.yandex.practicum.filmorate.exception.NotFoundException; -import ru.yandex.practicum.filmorate.exception.ValidationException; import ru.yandex.practicum.filmorate.model.User; +import ru.yandex.practicum.filmorate.storage.user.FriendsExtractor; import ru.yandex.practicum.filmorate.storage.user.UserStorage; -import java.util.Collection; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; @Service -@Slf4j -public class UserService { +@Slf4j(topic = "TRACE") +@RequiredArgsConstructor +public class UserService implements UserInterface { + + //sQL-запросы + private static final String SQL_SELECT_FRIENDS = "select userId, friendId from friends"; + private static final String SQL_INSERT_FRIEND = "insert into friends(userId, friendId) values (?, ?)"; + private static final String SQL_DELETE_FRIEND = "delete from friends where userId = ? and friendId = ?"; + + //сообщения для логирования и исключений + private static final String LOG_POST_REQUEST = "Обработка Post-запроса..."; + private static final String LOG_DELETE_REQUEST = "Обработка Del-запроса..."; + private static final String LOG_GET_REQUEST = "Обработка Get-запроса..."; + private static final String ERROR_FRIEND_ALREADY_ADDED = "Пользователь с id %d уже добавлен в друзья"; + private static final String ERROR_USER_NOT_FOUND = "Пользователь с данным идентификатором отсутствует в базе"; private final UserStorage userStorage; + private final JdbcTemplate jdbcTemplate; - @Autowired - public UserService(UserStorage userStorage) { - this.userStorage = userStorage; - } + @Override + public User addFriend(Long idUser, Long idFriend) { + log.info(LOG_POST_REQUEST); + + validateUserExists(idUser); + validateUserExists(idFriend); - public Collection findAll() { - return userStorage.findAll(); + if (isFriend(idUser, idFriend)) { + logAndThrowConditionsNotMetException(String.format(ERROR_FRIEND_ALREADY_ADDED, idFriend)); + } + + jdbcTemplate.update(SQL_INSERT_FRIEND, idUser, idFriend); + return userStorage.findById(idUser); } - public User create(User user) throws ValidationException, DuplicatedDataException { - return userStorage.create(user); + @Override + public User delFriend(Long idUser, Long idFriend) { + log.info(LOG_DELETE_REQUEST); + + validateUserExists(idUser); + validateUserExists(idFriend); + + jdbcTemplate.update(SQL_DELETE_FRIEND, idUser, idFriend); + return userStorage.findById(idUser); } - public User update(User user) throws NotFoundException, ValidationException { - return userStorage.update(user); + @Override + public Set findJointFriends(Long idUser, Long idFriend) { + log.info(LOG_GET_REQUEST); + + validateUserExists(idUser); + validateUserExists(idFriend); + + String sqlSelectJointFriends = "SELECT f1.friendId AS jointFriendId " + + "FROM friends f1 " + + "JOIN friends f2 ON f1.friendId = f2.friendId " + + "WHERE f1.userId = ? AND f2.userId = ?"; + + Set jointFriendIds = new HashSet<>(jdbcTemplate.queryForList( + sqlSelectJointFriends, Long.class, idUser, idFriend)); + + Set result = new HashSet<>(); + for (Long friendId : jointFriendIds) { + result.add(userStorage.findById(friendId)); + } + return result; } - public User findById(Long id) throws NotFoundException, ValidationException { - return userStorage.findById(id); + @Override + public Set findAllFriends(Long idUser) { + log.info(LOG_GET_REQUEST); + + validateUserExists(idUser); + + Map> friends = jdbcTemplate.query(SQL_SELECT_FRIENDS, new FriendsExtractor()); + assert friends != null; + Set userFriends = friends.getOrDefault(idUser, new HashSet<>()); + + Set result = new HashSet<>(); + for (Long friendId : userFriends) { + result.add(userStorage.findById(friendId)); + } + return result; } - public void addFriend(Long userId, Long friendId) throws NotFoundException { - userStorage.addFriend(userId, friendId); + private void validateUserExists(Long userId) { + if (userStorage.findById(userId) == null) { + logAndThrowNotFoundException(userId.toString()); + } } - public void removeFriend(Long userId, Long friendId) throws NotFoundException { - userStorage.removeFriend(userId, friendId); + private boolean isFriend(Long idUser, Long idFriend) { + Map> friends = jdbcTemplate.query(SQL_SELECT_FRIENDS, new FriendsExtractor()); + assert friends != null; + return friends.getOrDefault(idUser, new HashSet<>()).contains(idFriend); } - public Collection getFriends(Long id) throws NotFoundException { - return userStorage.getFriends(id); + private void logAndThrowConditionsNotMetException(String message) { + log.error("Exception", new ConditionsNotMetException(message)); + throw new ConditionsNotMetException(message); } - public Collection getCommonFriends(Long userId, Long otherUserId) throws NotFoundException { - return userStorage.getCommonFriends(userId, otherUserId); + private void logAndThrowNotFoundException(String value) { + log.error("Exception", new NotFoundException(UserService.ERROR_USER_NOT_FOUND)); + throw new NotFoundException(UserService.ERROR_USER_NOT_FOUND); } } \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java new file mode 100644 index 0000000..f1785c8 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java @@ -0,0 +1,283 @@ +package ru.yandex.practicum.filmorate.storage.film; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.ResultSetExtractor; +import org.springframework.jdbc.core.simple.SimpleJdbcInsert; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.exception.ConditionsNotMetException; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.model.*; +import ru.yandex.practicum.filmorate.service.GenreExtractor; +import ru.yandex.practicum.filmorate.service.RatingNameExtractor; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.LocalDate; +import java.util.*; +import java.util.stream.Collectors; + +@Repository +@RequiredArgsConstructor +@Slf4j(topic = "TRACE") +public class FilmDbStorage implements FilmStorage { + + //SQL-запросы + private static final String SQL_SELECT_GENRES = "select id, name from genre"; + private static final String SQL_SELECT_RATINGS = "select id, rating from filmrating"; + private static final String SQL_INSERT_FILM_GENRE = "insert into filmGenre(filmId, genreId) values (?, ?)"; + private static final String SQL_UPDATE_FILM_RATING = "update film set ratingId = ? where id = ?"; + private static final String SQL_UPDATE_FILM = "update film set name = ?, description = ?, releaseDate = ?, duration = ?, ratingId = ? where id = ?"; + + //сообщения для логирования и исключений + private static final String LOG_GET_REQUEST = "Обработка Get-запроса..."; + private static final String LOG_CREATE_REQUEST = "Обработка Create-запроса..."; + private static final String LOG_UPDATE_REQUEST = "Обработка Put-запроса..."; + private static final String ERROR_NULL_ID = "Идентификатор фильма не может быть нулевой"; + private static final String ERROR_FILM_NOT_FOUND = "Идентификатор фильма отсутствует в базе"; + private static final String ERROR_EMPTY_NAME = "Название не может быть пустым"; + private static final String ERROR_DESCRIPTION_LENGTH = "Максимальная длина описания — 200 символов"; + private static final String ERROR_RELEASE_DATE = "Дата релиза — не раньше 28 декабря 1895 года"; + private static final String ERROR_DURATION = "Продолжительность фильма должна быть положительным числом"; + private static final String ERROR_INVALID_RATING = "Некорректный рейтинг"; + private static final String ERROR_INVALID_GENRE = "Некорректный жанр"; + + private final JdbcTemplate jdbcTemplate; + + private Film mapRowToFilm(ResultSet resultSet, int rowNum) throws SQLException { + return Film.builder() + .id(resultSet.getLong("id")) + .name(resultSet.getString("name")) + .description(resultSet.getString("description")) + .releaseDate(resultSet.getDate("releaseDate").toLocalDate()) + .duration(resultSet.getInt("duration")) + .build(); + } + + public static class LikedUsersExtractor implements ResultSetExtractor>> { + @Override + public Map> extractData(ResultSet rs) throws SQLException { + Map> data = new LinkedHashMap<>(); + while (rs.next()) { + Long filmId = rs.getLong("filmId"); + data.putIfAbsent(filmId, new HashSet<>()); + Long userId = rs.getLong("userId"); + data.get(filmId).add(userId); + } + return data; + } + } + + public static class FilmGenreExtractor implements ResultSetExtractor>> { + @Override + public Map> extractData(ResultSet rs) throws SQLException { + Map> data = new LinkedHashMap<>(); + while (rs.next()) { + Long filmId = rs.getLong("filmId"); + data.putIfAbsent(filmId, new LinkedHashSet<>()); + Long genreId = rs.getLong("genreId"); + data.get(filmId).add(genreId); + } + return data; + } + } + + public static class FilmRatingExtractor implements ResultSetExtractor> { + @Override + public Map extractData(ResultSet rs) throws SQLException { + Map data = new HashMap<>(); + while (rs.next()) { + Long id = rs.getLong("id"); + data.putIfAbsent(id, 0L); + Long ratingId = rs.getLong("ratingId"); + data.put(id, ratingId); + } + return data; + } + } + + @Override + public List findAll() { + log.info(LOG_GET_REQUEST); + String sqlQuery = """ + SELECT f.id, f.name, f.description, f.releaseDate, f.duration, + GROUP_CONCAT(DISTINCT fg.genreId) AS genreIds, + GROUP_CONCAT(DISTINCT lu.userId) AS likedUserIds, + f.ratingId + FROM film f + LEFT JOIN filmGenre fg ON f.id = fg.filmId + LEFT JOIN likedUsers lu ON f.id = lu.filmId + GROUP BY f.id + """; + + List films = jdbcTemplate.query(sqlQuery, (rs, rowNum) -> { + Film film = mapRowToFilm(rs, rowNum); + + String genreIds = rs.getString("genreIds"); + if (genreIds != null) { + LinkedHashSet genres = Arrays.stream(genreIds.split(",")) + .map(Long::parseLong) + .collect(Collectors.toCollection(LinkedHashSet::new)); + film.setGenres(genres); + } + + String likedUserIds = rs.getString("likedUserIds"); + if (likedUserIds != null) { + Set likedUsers = Arrays.stream(likedUserIds.split(",")) + .map(Long::parseLong) + .collect(Collectors.toSet()); + film.setLikedUsers(likedUsers); + } + + film.setMpa(rs.getLong("ratingId")); + + return film; + }); + + return films; + } + + + @Override + public FilmResponse findById(Long id) { + log.info(LOG_GET_REQUEST); + if (id == null || id == 0) { + logAndThrowConditionsNotMetException(ERROR_NULL_ID); + } + + String sqlQuery5 = "select id, name, description, releaseDate, duration from film where id = ?"; + try { + jdbcTemplate.queryForObject(sqlQuery5, this::mapRowToFilm, id); + } catch (DataAccessException e) { + + logAndThrowNotFoundException(id.toString(), ERROR_FILM_NOT_FOUND); + } + + Film film = jdbcTemplate.queryForObject(sqlQuery5, this::mapRowToFilm, id); + String sqlQuery6 = "select filmId, userId from likedUsers where filmId = ?"; + Map> likedUsers = jdbcTemplate.query(sqlQuery6, new LikedUsersExtractor(), id); + String sqlQuery7 = "select filmId, genreId from filmGenre where filmId = ?"; + Map> filmGenre = jdbcTemplate.query(sqlQuery7, new FilmGenreExtractor(), id); + String sqlQuery8 = "select id, ratingId from film where id = ?"; + Map filmRating = jdbcTemplate.query(sqlQuery8, new FilmRatingExtractor(), id); + + + film.setLikedUsers(likedUsers.get(id)); + + film.setGenres(filmGenre.get(id)); + Map genre = jdbcTemplate.query(SQL_SELECT_GENRES, new GenreExtractor()); + Map rating = jdbcTemplate.query(SQL_SELECT_RATINGS, new RatingNameExtractor()); + LinkedHashSet genres = new LinkedHashSet<>(); + if (!filmGenre.isEmpty()) { + for (Long g : filmGenre.get(id)) { + genres.add(Genre.of(g, genre.get(g))); + } + } + + film.setMpa(filmRating.get(id)); + + return FilmResponse.of(film.getId(), film.getName(), film.getDescription(), film.getReleaseDate(), film.getDuration(), new HashSet<>(), Mpa.of(film.getMpa(), rating.get(film.getMpa())), genres); + } + + @Override + public FilmResponse create(@Valid Buffer buffer) { + log.info(LOG_CREATE_REQUEST); + validateBuffer(buffer); + + SimpleJdbcInsert simpleJdbcInsert = new SimpleJdbcInsert(jdbcTemplate).withTableName("film").usingGeneratedKeyColumns("id"); + Long filmId = simpleJdbcInsert.executeAndReturnKey(buffer.toMapBuffer()).longValue(); + + Map genre = jdbcTemplate.query(SQL_SELECT_GENRES, new GenreExtractor()); + Map rating = jdbcTemplate.query(SQL_SELECT_RATINGS, new RatingNameExtractor()); + + LinkedHashSet genres = processGenres(buffer.getGenres(), filmId, genre); + updateFilmRating(buffer.getMpa(), filmId); + + return FilmResponse.of(filmId, buffer.getName(), buffer.getDescription(), buffer.getReleaseDate(), buffer.getDuration(), new HashSet<>(), Mpa.of(buffer.getMpa(), rating.get(buffer.getMpa())), genres); + } + + @Override + public FilmResponse update(@Valid Buffer newFilm) { + log.info(LOG_UPDATE_REQUEST); + if (newFilm.getId() == null) { + logAndThrowConditionsNotMetException("Id должен быть указан"); + } + + FilmResponse oldFilm = findById(newFilm.getId()); + validateBuffer(newFilm); + + oldFilm.setName(newFilm.getName()); + oldFilm.setDescription(newFilm.getDescription()); + oldFilm.setReleaseDate(newFilm.getReleaseDate()); + oldFilm.setDuration(newFilm.getDuration()); + + Map genre = jdbcTemplate.query(SQL_SELECT_GENRES, new GenreExtractor()); + Map rating = jdbcTemplate.query(SQL_SELECT_RATINGS, new RatingNameExtractor()); + + LinkedHashSet genres = processGenres(newFilm.getGenres(), oldFilm.getId(), genre); + updateFilmRating(newFilm.getMpa(), oldFilm.getId()); + + jdbcTemplate.update(SQL_UPDATE_FILM, oldFilm.getName(), oldFilm.getDescription(), oldFilm.getReleaseDate(), + oldFilm.getDuration(), oldFilm.getMpa().getId(), oldFilm.getId()); + + return FilmResponse.of(oldFilm.getId(), oldFilm.getName(), oldFilm.getDescription(), oldFilm.getReleaseDate(), + oldFilm.getDuration(), new HashSet<>(), Mpa.of(newFilm.getMpa(), rating.get(newFilm.getMpa())), genres); + } + + private void validateBuffer(Buffer buffer) { + if (buffer.getName() == null || buffer.getName().isBlank()) { + logAndThrowConditionsNotMetException(ERROR_EMPTY_NAME); + } + + if (buffer.getDescription().length() > 200) { + logAndThrowConditionsNotMetException(ERROR_DESCRIPTION_LENGTH); + } + + if (buffer.getReleaseDate().isBefore(LocalDate.of(1895, 12, 28))) { + logAndThrowConditionsNotMetException(ERROR_RELEASE_DATE); + } + + if (buffer.getDuration() == null || buffer.getDuration() <= 0) { + logAndThrowConditionsNotMetException(ERROR_DURATION); + } + + if (!(buffer.getMpa() > 0 && buffer.getMpa() < 6)) { + logAndThrowNotFoundException(buffer.getMpa().toString(), ERROR_INVALID_RATING); + } + } + + private LinkedHashSet processGenres(List genres, Long filmId, Map genreMap) { + LinkedHashSet result = new LinkedHashSet<>(); + if (genres == null || genres.equals(List.of("нет жанра"))) { + return result; + } + + for (String genreIdStr : genres) { + Long genreId = Long.parseLong(genreIdStr); + if (!genreMap.containsKey(genreId)) { + logAndThrowNotFoundException(genreId.toString(), ERROR_INVALID_GENRE); + } + jdbcTemplate.update(SQL_INSERT_FILM_GENRE, filmId, genreId); + result.add(Genre.of(genreId, genreMap.get(genreId))); + } + return result; + } + + + private void updateFilmRating(Long mpaId, Long filmId) { + jdbcTemplate.update(SQL_UPDATE_FILM_RATING, mpaId, filmId); + } + + private void logAndThrowConditionsNotMetException(String message) { + log.error("Exception", new ConditionsNotMetException(message)); + throw new ConditionsNotMetException(message); + } + + private void logAndThrowNotFoundException(String value, String message) { + log.error("Exception", new NotFoundException(message)); + throw new NotFoundException(message); + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmStorage.java index a6f329d..9b12145 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmStorage.java @@ -1,25 +1,18 @@ package ru.yandex.practicum.filmorate.storage.film; -import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.model.Buffer; import ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.model.FilmResponse; -import java.util.Collection; import java.util.List; public interface FilmStorage { - Collection findAll(); + List findAll(); - Film findById(Long id) throws NotFoundException; + FilmResponse findById(Long id); - Film create(Film film); + FilmResponse create(Buffer film); - Film update(Film film) throws NotFoundException; - - //Film - void addLike(Long filmId, Long userId) throws NotFoundException; - - void removeLike(Long filmId, Long userId) throws NotFoundException; - - List getTopFilms(int count); + FilmResponse update(Buffer newFilm); } \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java deleted file mode 100644 index f058161..0000000 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java +++ /dev/null @@ -1,133 +0,0 @@ -package ru.yandex.practicum.filmorate.storage.film; - -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; -import ru.yandex.practicum.filmorate.exception.ExceptionMessages; -import ru.yandex.practicum.filmorate.exception.NotFoundException; -import ru.yandex.practicum.filmorate.exception.ValidationException; -import ru.yandex.practicum.filmorate.model.Film; -import ru.yandex.practicum.filmorate.model.User; -import ru.yandex.practicum.filmorate.storage.user.UserStorage; - -import java.time.LocalDateTime; -import java.time.chrono.ChronoLocalDate; -import java.util.*; -import java.util.stream.Collectors; - -@Component -@Slf4j -public class InMemoryFilmStorage implements FilmStorage { - - private static final Map films = new HashMap<>(); - private final UserStorage userStorage; - - @Autowired - public InMemoryFilmStorage(UserStorage userStorage) { - this.userStorage = userStorage; - } - - @Override - public Collection findAll() { - log.info("Processing Get-request..."); - return films.values(); - } - - @Override - public Film findById(Long id) throws NotFoundException { - if (id == null) { - throw new ValidationException(ExceptionMessages.FILM_ID_CANNOT_BE_NULL); - } - Film film = films.get(id); - if (film == null) { - throw new NotFoundException(String.format(ExceptionMessages.FILM_NOT_FOUND, id)); - } - return film; - } - - @Override - public Film create(Film film) throws ValidationException { - validateFilm(film); - film.setId(getNextId()); - films.put(film.getId(), film); - return film; - } - - @Override - public Film update(Film film) throws NotFoundException, ValidationException { - validateFilm(film); - Film oldFilm = films.get(film.getId()); - if (oldFilm == null) { - throw new NotFoundException(String.format(ExceptionMessages.FILM_NOT_FOUND, film.getId())); - } - oldFilm.setName(film.getName()); - oldFilm.setDescription(film.getDescription()); - oldFilm.setReleaseDate(film.getReleaseDate()); - oldFilm.setDuration(film.getDuration()); - return oldFilm; - } - - @Override - public void addLike(Long filmId, Long userId) throws NotFoundException { - Film film = findById(filmId); // Проверяем, существует ли фильм - if (film == null) { - throw new NotFoundException(String.format(ExceptionMessages.FILM_NOT_FOUND, filmId)); - } - - User user = userStorage.findById(userId); - if (user == null) { - throw new NotFoundException(String.format("not found", userId)); - } - - film.getLikedUsers().add(userId); - log.info("User with ID = {} liked the film with ID = {}", userId, filmId); - } - - @Override - public void removeLike(Long filmId, Long userId) throws NotFoundException { - Film film = findById(filmId); - if (film == null) { - throw new NotFoundException(String.format(ExceptionMessages.FILM_NOT_FOUND, filmId)); - } - - User user = userStorage.findById(userId); - if (user == null) { - throw new NotFoundException(String.format("not found", userId)); - } - - if (!film.getLikedUsers().contains(userId)) { - throw new NotFoundException(String.format("User with ID = %d did not like the film with ID = %d", userId, filmId)); - } - - film.getLikedUsers().remove(userId); - log.info("User with ID = {} unliked the film with ID = {}", userId, filmId); - } - - @Override - public List getTopFilms(int count) { - log.info("Getting top-{} films by number of likes", count); - return films.values().stream() - .sorted(Comparator.comparingInt(f -> -f.getLikedUsers().size())) - .limit(count) - .collect(Collectors.toList()); - } - - private long getNextId() { - return films.keySet().stream().mapToLong(id -> id).max().orElse(0) + 1; - } - - private void validateFilm(Film film) throws ValidationException { - if (film.getName() == null || film.getName().isBlank()) { - throw new ValidationException(ExceptionMessages.FILM_NAME_CANNOT_BE_EMPTY); - } - if (film.getDescription() != null && film.getDescription().length() > 200) { - throw new ValidationException(ExceptionMessages.FILM_DESCRIPTION_TOO_LONG); - } - if (film.getReleaseDate().isBefore(ChronoLocalDate.from(LocalDateTime.of(1895, 12, 28, 0, 0, 0)))) { - throw new ValidationException(ExceptionMessages.FILM_RELEASE_DATE_INVALID); - } - if (film.getDuration() <= 0) { - throw new ValidationException(ExceptionMessages.FILM_DURATION_INVALID); - } - } -} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/genre/GenreDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/genre/GenreDbStorage.java new file mode 100644 index 0000000..03f756a --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/genre/GenreDbStorage.java @@ -0,0 +1,35 @@ +package ru.yandex.practicum.filmorate.storage.genre; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.model.Genre; + +import java.util.List; + +@Repository +public class GenreDbStorage implements GenreStorage { + + private final JdbcTemplate jdbcTemplate; + + @Autowired + public GenreDbStorage(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + public Genre getGenreById(Long id) { + String sql = "SELECT id, name FROM genre WHERE id = ?"; + try { + return jdbcTemplate.queryForObject(sql, (rs, rowNum) -> Genre.of(rs.getLong("id"), rs.getString("name")), id); + } catch (DataAccessException e) { + throw new NotFoundException("Genre with id " + id + " not found"); + } + } + + public List getAllGenres() { + String sql = "SELECT id, name FROM genre"; + return jdbcTemplate.query(sql, (rs, rowNum) -> Genre.of(rs.getLong("id"), rs.getString("name"))); + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/genre/GenreStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/genre/GenreStorage.java new file mode 100644 index 0000000..f4f10dd --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/genre/GenreStorage.java @@ -0,0 +1,13 @@ +package ru.yandex.practicum.filmorate.storage.genre; + +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.model.Genre; + +import java.util.List; + +public interface GenreStorage { + + Genre getGenreById(Long id) throws NotFoundException; + + List getAllGenres(); +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/mpa/MpaDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/mpa/MpaDbStorage.java new file mode 100644 index 0000000..0800d79 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/mpa/MpaDbStorage.java @@ -0,0 +1,35 @@ +package ru.yandex.practicum.filmorate.storage.mpa; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.model.Mpa; + +import java.util.List; + +@Repository +public class MpaDbStorage implements MpaStorage { + + private final JdbcTemplate jdbcTemplate; + + @Autowired + public MpaDbStorage(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + public Mpa getMpaById(Long id) { + String sql = "SELECT id, rating FROM filmrating WHERE id = ?"; + try { + return jdbcTemplate.queryForObject(sql, (rs, rowNum) -> Mpa.of(rs.getLong("id"), rs.getString("rating")), id); + } catch (DataAccessException e) { + throw new NotFoundException("MPA with id " + id + " not found"); + } + } + + public List getAllMpa() { + String sql = "SELECT id, rating FROM filmrating"; + return jdbcTemplate.query(sql, (rs, rowNum) -> Mpa.of(rs.getLong("id"), rs.getString("rating"))); + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/mpa/MpaStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/mpa/MpaStorage.java new file mode 100644 index 0000000..0e852aa --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/mpa/MpaStorage.java @@ -0,0 +1,13 @@ +package ru.yandex.practicum.filmorate.storage.mpa; + +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.model.Mpa; + +import java.util.List; + +public interface MpaStorage { + + Mpa getMpaById(Long id) throws NotFoundException; + + List getAllMpa(); +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/user/EmailExtractor.java b/src/main/java/ru/yandex/practicum/filmorate/storage/user/EmailExtractor.java new file mode 100644 index 0000000..3587d50 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/user/EmailExtractor.java @@ -0,0 +1,19 @@ +package ru.yandex.practicum.filmorate.storage.user; + +import org.springframework.jdbc.core.ResultSetExtractor; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.*; + +public class EmailExtractor implements ResultSetExtractor> { + + @Override + public Set extractData(ResultSet rs) throws SQLException { + Set data = new HashSet<>(); + while (rs.next()) { + String email = rs.getString("email"); + data.add(email); + } + return data; + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/user/FriendsExtractor.java b/src/main/java/ru/yandex/practicum/filmorate/storage/user/FriendsExtractor.java new file mode 100644 index 0000000..fb5b074 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/user/FriendsExtractor.java @@ -0,0 +1,21 @@ +package ru.yandex.practicum.filmorate.storage.user; + +import org.springframework.jdbc.core.ResultSetExtractor; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.*; + +public class FriendsExtractor implements ResultSetExtractor>> { + + @Override + public Map> extractData(ResultSet rs) throws SQLException { + Map> data = new LinkedHashMap<>(); + while (rs.next()) { + Long userId = rs.getLong("userId"); + data.putIfAbsent(userId, new HashSet<>()); + Long friendId = rs.getLong("friendId"); + data.get(userId).add(friendId); + } + return data; + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java deleted file mode 100644 index fbcbdf5..0000000 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java +++ /dev/null @@ -1,183 +0,0 @@ -package ru.yandex.practicum.filmorate.storage.user; - -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import ru.yandex.practicum.filmorate.exception.DuplicatedDataException; -import ru.yandex.practicum.filmorate.exception.NotFoundException; -import ru.yandex.practicum.filmorate.exception.ValidationException; -import ru.yandex.practicum.filmorate.model.User; - -import java.time.LocalDate; -import java.util.*; -import java.util.stream.Collectors; - -@Component -@Slf4j -public class InMemoryUserStorage implements UserStorage { - private final Map users = new HashMap<>(); - - @Override - public Collection findAll() { - log.info("Returning the list of users..."); - return users.values(); - } - - @Override - public User create(User user) throws ValidationException, DuplicatedDataException { - validateEmail(user.getEmail()); - validateLogin(user.getLogin()); - if (user.getName() == null || user.getName().isBlank()) { - user.setName(user.getLogin()); - } - validateBirthday(user.getBirthday()); - duplicateCheck(user); - user.setId(getNextId()); - user.setFriends(new HashSet<>()); - users.put(user.getId(), user); - log.info("User created: {}", user); - return user; - } - - @Override - public User update(User newUser) throws NotFoundException, ValidationException { - if (newUser.getId() == null) { - throw new ValidationException("User ID cannot be null"); - } - if (!users.containsKey(newUser.getId())) { - throw new NotFoundException("User with ID = " + newUser.getId() + " not found"); - } - User oldUser = users.get(newUser.getId()); - oldUser.setEmail(newUser.getEmail()); - oldUser.setLogin(newUser.getLogin()); - oldUser.setName(newUser.getName() != null ? newUser.getName() : newUser.getLogin()); - oldUser.setBirthday(newUser.getBirthday()); - - if (newUser.getFriends() == null) { - newUser.setFriends(new HashSet<>()); - } - oldUser.setFriends(new HashSet<>(newUser.getFriends())); - - users.put(oldUser.getId(), oldUser); - log.info("User with ID = {} updated: {}", oldUser.getId(), oldUser); - return oldUser; - } - - @Override - public User findById(Long id) throws NotFoundException { - if (id == null) { - throw new ValidationException("ID cannot be null"); - } - User user = users.get(id); - if (user == null) { - throw new NotFoundException("User with ID = " + id + " not found"); - } - log.info("User found: {}", user); - return user; - } - - @Override - public void addFriend(Long userId, Long friendId) throws NotFoundException { - User user = findById(userId); - User friend = findById(friendId); - - if (user.getFriends() == null) { - user.setFriends(new HashSet<>()); - } - if (friend.getFriends() == null) { - friend.setFriends(new HashSet<>()); - } - - user.getFriends().add(friendId); - friend.getFriends().add(userId); - - update(user); - update(friend); - - log.info("User with ID = {} added as a friend to user with ID = {}", friendId, userId); - } - - @Override - public User removeFriend(Long userId, Long friendId) throws NotFoundException { - User user = findById(userId); - User friend = findById(friendId); - - if (user.getFriends() == null) { - user.setFriends(new HashSet<>()); - } - if (friend.getFriends() == null) { - friend.setFriends(new HashSet<>()); - } - - user.getFriends().remove(friendId); - friend.getFriends().remove(userId); - - log.info("User with ID = {} has been removed from friends of user with ID = {}", friendId, userId); - return user; - } - - @Override - public Collection getFriends(Long id) throws NotFoundException { - User user = findById(id); - - if (user.getFriends() == null || user.getFriends().isEmpty()) { - return Collections.emptyList(); - } - - return user.getFriends().stream() - .map(this::findById) - .collect(Collectors.toList()); - } - - @Override - public Collection getCommonFriends(Long userId, Long otherUserId) throws NotFoundException { - User user = findById(userId); - User otherUser = findById(otherUserId); - - if (user.getFriends() == null) { - user.setFriends(new HashSet<>()); - } - if (otherUser.getFriends() == null) { - otherUser.setFriends(new HashSet<>()); - } - - Set commonFriendIds = new HashSet<>(user.getFriends()); - commonFriendIds.retainAll(otherUser.getFriends()); - - return commonFriendIds.stream() - .map(this::findById) - .collect(Collectors.toList()); - } - - private long getNextId() { - return users.keySet().stream().mapToLong(id -> id).max().orElse(0) + 1; - } - - private void duplicateCheck(User user) throws DuplicatedDataException { - for (User u : users.values()) { - if (u.getEmail().equals(user.getEmail())) { - throw new DuplicatedDataException("A user with this email already exists"); - } - } - } - - private void validateEmail(String email) throws ValidationException { - if (email == null || email.isBlank() || !email.contains("@") || email.contains(" ") || email.length() < 2) { - throw new ValidationException("Invalid email"); - } - } - - private void validateLogin(String login) throws ValidationException { - if (login == null || login.isBlank() || login.contains(" ")) { - throw new ValidationException("Login cannot be empty or contain spaces"); - } - } - - private void validateBirthday(LocalDate birthday) throws ValidationException { - if (birthday == null) { - throw new ValidationException("Birthday cannot be null"); - } - if (birthday.isAfter(LocalDate.now())) { - throw new ValidationException("Birthday cannot be in the future"); - } - } -} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorage.java new file mode 100644 index 0000000..690f0f3 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorage.java @@ -0,0 +1,167 @@ +package ru.yandex.practicum.filmorate.storage.user; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.simple.SimpleJdbcInsert; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.exception.ConditionsNotMetException; +import ru.yandex.practicum.filmorate.exception.DuplicatedDataException; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.exception.ValidationException; +import ru.yandex.practicum.filmorate.model.User; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.*; + +@Repository +@RequiredArgsConstructor +@Slf4j(topic = "TRACE") +public class UserDbStorage implements UserStorage { + + private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + private final JdbcTemplate jdbcTemplate; + + // Переименованные запросы в camelCase + private final String selectAllUsers = "SELECT id, name, email, login, birthday FROM users"; + private final String selectAllFriendships = "SELECT userId, friendId FROM friends"; + private final String selectUserById = "SELECT id, name, email, login, birthday FROM users WHERE id = ?"; + private final String selectAllEmails = "SELECT email FROM users"; + private final String updateUser = "UPDATE users SET name = ?, email = ?, login = ?, birthday = ? WHERE id = ?"; + + private User mapRowToUser(ResultSet resultSet, int rowNum) throws SQLException { + return User.builder() + .id(resultSet.getLong("id")) + .name(resultSet.getString("name")) + .email(resultSet.getString("email")) + .login(resultSet.getString("login")) + .birthday(resultSet.getDate("birthday").toLocalDate()) + .friends(new HashSet<>()) + .friendRequests(new HashSet<>()) + .build(); + } + + @Override + public Collection findAll() { + log.info("Обработка Get-запроса..."); + Collection users = jdbcTemplate.query(selectAllUsers, this::mapRowToUser); + Map> friends = jdbcTemplate.query(selectAllFriendships, new FriendsExtractor()); + for (User user : users) { + user.setFriends(friends.get(user.getId())); + } + return users; + } + + @Override + public User findById(Long id) { + log.info("Обработка Get-запроса..."); + if (id != 0 && id != null) { + try { + jdbcTemplate.queryForObject(selectUserById, this::mapRowToUser, id); + } catch (DataAccessException e) { + log.error("Exception", new NotFoundException("Пользователь с данным идентификатором отсутствует в базе")); + throw new NotFoundException("Пользователь с данным идентификатором отсутствует в базе"); + } + User user = jdbcTemplate.queryForObject(selectUserById, this::mapRowToUser, id); + Map> friends = jdbcTemplate.query(selectAllFriendships, new FriendsExtractor()); + user.setFriends(friends.get(id)); + return user; + } else { + log.error("Exception", new ConditionsNotMetException("Идентификатор пользователя не может быть нулевой")); + throw new ConditionsNotMetException("Идентификатор пользователя не может быть нулевой"); + } + } + + @Override + public User create(@Valid User user) { + log.info("Обработка Create-запроса..."); + + duplicateCheck(user); + + if (user.getEmail() == null || user.getEmail().isBlank() || !user.getEmail().contains("@")) { + throw new ValidationException("Электронная почта не может быть пустой и должна содержать символ @"); + } + + if (user.getLogin() == null || user.getLogin().isBlank() || user.getLogin().contains(" ")) { + throw new ValidationException("Логин не может быть пустым и содержать пробелы"); + } + + if (user.getName() == null || user.getName().isBlank()) { + user.setName(user.getLogin()); + } + + if (user.getBirthday() == null) { + throw new ValidationException("Дата рождения не может быть нулевой"); + } + if (user.getBirthday().isAfter(LocalDate.now())) { + throw new ValidationException("Дата рождения не может быть в будущем"); + } + + SimpleJdbcInsert simpleJdbcInsert = new SimpleJdbcInsert(jdbcTemplate) + .withTableName("users") + .usingGeneratedKeyColumns("id"); + Long id = simpleJdbcInsert.executeAndReturnKey(user.toMapUser()).longValue(); + user.setId(id); + + return user; + } + + private void duplicateCheck(User user) { + Set emails = jdbcTemplate.query(selectAllEmails, new EmailExtractor()); + if (emails.contains(user.getEmail())) { + log.error("Exception", new DuplicatedDataException("Этот имейл уже используется")); + throw new DuplicatedDataException("Этот имейл уже используется"); + } + } + + @Override + public User update(@Valid User newUser) { + log.info("Обработка Update-запроса..."); + + if (newUser.getId() == null) { + throw new ValidationException("Id должен быть указан"); + } + + User oldUser = findById(newUser.getId()); + if (oldUser == null) { + throw new NotFoundException("Пользователь с указанным id не найден"); + } + + if (newUser.getEmail() == null || newUser.getEmail().isBlank() || !newUser.getEmail().contains("@")) { + throw new ValidationException("Электронная почта не может быть пустой и должна содержать символ @"); + } + + if (!newUser.getEmail().equals(oldUser.getEmail())) { + duplicateCheck(newUser); + } + + if (newUser.getLogin() == null || newUser.getLogin().isBlank() || newUser.getLogin().contains(" ")) { + throw new ValidationException("Логин не может быть пустым и содержать пробелы"); + } + + if (newUser.getName() == null || newUser.getName().isBlank()) { + newUser.setName(newUser.getLogin()); + } + + if (newUser.getBirthday() == null) { + throw new ValidationException("Дата рождения не может быть нулевой"); + } + if (newUser.getBirthday().isAfter(LocalDate.now())) { + throw new ValidationException("Дата рождения не может быть в будущем"); + } + + jdbcTemplate.update(updateUser, + newUser.getName(), + newUser.getEmail(), + newUser.getLogin(), + newUser.getBirthday(), + newUser.getId()); + + return newUser; + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserStorage.java index 20710ad..28c4979 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserStorage.java @@ -1,6 +1,5 @@ package ru.yandex.practicum.filmorate.storage.user; -import ru.yandex.practicum.filmorate.exception.NotFoundException; import ru.yandex.practicum.filmorate.model.User; import java.util.Collection; @@ -9,17 +8,9 @@ public interface UserStorage { Collection findAll(); - User create(User user); - - User update(User user) throws NotFoundException; - - void addFriend(Long userId, Long friendId) throws NotFoundException; + User findById(Long id); - User removeFriend(Long userId, Long friendId) throws NotFoundException; - - Collection getCommonFriends(Long userId, Long otherUserId) throws NotFoundException; - - User findById(Long id) throws NotFoundException; + User create(User user); - Collection getFriends(Long id) throws NotFoundException; + User update(User newUser); } \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 8b13789..533c651 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,6 @@ - +#logging.level.org.zalando.logbook=TRACE +spring.sql.init.mode=always +spring.datasource.url=jdbc:h2:file:./db/filmorate +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password=password diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql new file mode 100644 index 0000000..f56133d --- /dev/null +++ b/src/main/resources/data.sql @@ -0,0 +1,15 @@ +INSERT INTO filmrating (rating) + VALUES + ('G'), + ('PG'), + ('PG-13'), + ('R'), + ('NC-17'); +INSERT INTO genre (name) + VALUES + ('Комедия'), + ('Драма'), + ('Мультфильм'), + ('Триллер'), + ('Документальный'), + ('Боевик'); \ No newline at end of file diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql new file mode 100644 index 0000000..7256b2b --- /dev/null +++ b/src/main/resources/schema.sql @@ -0,0 +1,59 @@ +CREATE TABLE IF NOT EXISTS filmrating ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + rating Varchar NOT NULL +); + +CREATE TABLE IF NOT EXISTS film ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + name Varchar NOT NULL, + description Varchar NOT NULL, + releaseDate Date, + duration INT, + ratingId BIGINT NOT NULL, + FOREIGN KEY (ratingId) REFERENCES filmrating (id) +); + +CREATE TABLE IF NOT EXISTS genre ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + name Varchar NOT NULL +); + +CREATE TABLE IF NOT EXISTS filmGenre ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + filmId BIGINT NOT NULL, + genreId BIGINT NOT NULL, + FOREIGN KEY (genreId) REFERENCES genre (id), + FOREIGN KEY (filmId) REFERENCES film (id) +); + +CREATE TABLE IF NOT EXISTS users ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + name Varchar NOT NULL, + email Varchar NOT NULL, + login Varchar NOT NULL, + birthday Date +); + +CREATE TABLE IF NOT EXISTS friends ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + userId BIGINT NOT NULL, + friendId BIGINT NOT NULL, + FOREIGN KEY (userId) REFERENCES users (id), + FOREIGN KEY (friendId) REFERENCES users (id) +); + +CREATE TABLE IF NOT EXISTS friendRequest ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + userId BIGINT NOT NULL, + friendId BIGINT NOT NULL, + accept BOOLEAN, + FOREIGN KEY (userId) REFERENCES users (id) +); + +CREATE TABLE IF NOT EXISTS likedUsers ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + filmId BIGINT NOT NULL, + userId BIGINT NOT NULL, + FOREIGN KEY (filmId) REFERENCES film (id), + FOREIGN KEY (userId) REFERENCES users (id) +); \ No newline at end of file diff --git a/src/main/resources/schema.sql.bak b/src/main/resources/schema.sql.bak new file mode 100644 index 0000000..f1d7d94 --- /dev/null +++ b/src/main/resources/schema.sql.bak @@ -0,0 +1,51 @@ +CREATE TABLE IF NOT EXISTS filmrating ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + rating Varchar NOT NULL + ); + CREATE TABLE IF NOT EXISTS film ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + name Varchar NOT NULL, + description Varchar NOT NULL, + releaseDate Date, + duration INT, + rating_id BIGINT NOT NULL, + FOREIGN KEY (rating_id) REFERENCES filmrating (id) + ); + CREATE TABLE IF NOT EXISTS genre ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + name Varchar NOT NULL + ); + CREATE TABLE IF NOT EXISTS filmGenre ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + filmId BIGINT NOT NULL, + genreId BIGINT NOT NULL, + FOREIGN KEY (genreId) REFERENCES genre (id), + FOREIGN KEY (filmId) REFERENCES film (id) + ); + CREATE TABLE IF NOT EXISTS users ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + name Varchar NOT NULL, + email Varchar NOT NULL, + login Varchar NOT NULL, + birthday Date + ); + CREATE TABLE IF NOT EXISTS friends ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + userId BIGINT NOT NULL, + friendId BIGINT NOT NULL, + FOREIGN KEY (userId) REFERENCES users (id) + ); + CREATE TABLE IF NOT EXISTS friendRequest ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + userId BIGINT NOT NULL, + friendId BIGINT NOT NULL, + accept BOOLEAN, + FOREIGN KEY (userId) REFERENCES users (id) + ); + CREATE TABLE IF NOT EXISTS likedUsers ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + filmId BIGINT NOT NULL, + userId BIGINT NOT NULL, + FOREIGN KEY (filmId) REFERENCES film (id), + FOREIGN KEY (userId) REFERENCES users (id) + ); \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/FilmControllerTest.java b/src/test/java/ru/yandex/practicum/filmorate/FilmControllerTest.java deleted file mode 100644 index 1e3ef4e..0000000 --- a/src/test/java/ru/yandex/practicum/filmorate/FilmControllerTest.java +++ /dev/null @@ -1,126 +0,0 @@ -package ru.yandex.practicum.filmorate; - -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; -import ru.yandex.practicum.filmorate.exception.NotFoundException; -import ru.yandex.practicum.filmorate.exception.ValidationException; -import ru.yandex.practicum.filmorate.model.Film; -import ru.yandex.practicum.filmorate.service.FilmService; -import ru.yandex.practicum.filmorate.controller.FilmController; -import ru.yandex.practicum.filmorate.storage.film.FilmStorage; -import ru.yandex.practicum.filmorate.storage.film.InMemoryFilmStorage; -import ru.yandex.practicum.filmorate.storage.user.UserStorage; - -import java.time.LocalDate; -import java.time.format.DateTimeFormatter; - -import static org.junit.jupiter.api.Assertions.*; - -public class FilmControllerTest { - - private static FilmController filmController; - private static Film validFilm; - private static Film invalidFilmName; - private static Film invalidFilmDescription; - private static Film invalidFilmReleaseDate; - private static Film filmWithNoId; - private static Film filmWithWrongId; - - @BeforeAll - public static void start() throws ValidationException { - // Создаем мок для UserStorage - UserStorage userStorage = Mockito.mock(UserStorage.class); - - // Создаем экземпляр хранилища фильмов, передавая мок UserStorage - FilmStorage filmStorage = new InMemoryFilmStorage(userStorage); - - // Создаем экземпляр сервиса, передавая ему хранилище - FilmService filmService = new FilmService(filmStorage); - - // Создаем контроллер, передавая ему сервис - filmController = new FilmController(filmService); - - // Инициализация тестовых данных - validFilm = new Film(); - validFilm.setId(0L); - validFilm.setName("Фильм"); - validFilm.setDescription("Описание"); - validFilm.setReleaseDate(LocalDate.parse("2020-04-19", DateTimeFormatter.ofPattern("yyyy-MM-dd"))); - validFilm.setDuration(100); - filmController.createFilm(validFilm); - - invalidFilmName = new Film(); - invalidFilmName.setId(0L); - invalidFilmName.setName(" "); - invalidFilmName.setDescription("Описание фильма без имени"); - invalidFilmName.setReleaseDate(LocalDate.parse("2020-04-19", DateTimeFormatter.ofPattern("yyyy-MM-dd"))); - invalidFilmName.setDuration(100); - - invalidFilmDescription = new Film(); - invalidFilmDescription.setId(0L); - invalidFilmDescription.setName("Название фильма"); - invalidFilmDescription.setDescription("Длинное длинное очень длинное описание " + - "Чтобы проверить, как это все дело работает"); - invalidFilmDescription.setReleaseDate(LocalDate.parse("2020-04-19", DateTimeFormatter.ofPattern("yyyy-MM-dd"))); - invalidFilmDescription.setDuration(100); - - invalidFilmReleaseDate = new Film(); - invalidFilmReleaseDate.setId(0L); - invalidFilmReleaseDate.setName("Фильм с неправильной датой"); - invalidFilmReleaseDate.setDescription("Описание фильма с неправильной датой"); - invalidFilmReleaseDate.setReleaseDate(LocalDate.parse("1880-04-19", DateTimeFormatter.ofPattern("yyyy-MM-dd"))); - invalidFilmReleaseDate.setDuration(100); - - filmWithNoId = new Film(); - filmWithNoId.setId(null); - filmWithNoId.setName("Фильм без айди"); - filmWithNoId.setDescription("Описание фильма без айди"); - filmWithNoId.setReleaseDate(LocalDate.parse("2020-04-19", DateTimeFormatter.ofPattern("yyyy-MM-dd"))); - filmWithNoId.setDuration(100); - - filmWithWrongId = new Film(); - filmWithWrongId.setId(50L); - filmWithWrongId.setName("Фильм с неправильным айди"); - filmWithWrongId.setDescription("Описание фильма с неправильным айди"); - filmWithWrongId.setReleaseDate(LocalDate.parse("2020-04-19", DateTimeFormatter.ofPattern("yyyy-MM-dd"))); - filmWithWrongId.setDuration(100); - } - - @Test - public void shouldCreateValidFilm() throws ValidationException { - assertEquals(filmController.createFilm(validFilm), validFilm); - } - - @Test - public void shouldThrowExceptionWhenCreatingFilmWithEmptyName() { - ValidationException exception = assertThrows(ValidationException.class, () -> { - filmController.createFilm(invalidFilmName); - }); - assertNotNull(exception); - assertEquals("Film name cannot be empty", exception.getMessage()); - } - - @Test - public void shouldThrowExceptionWhenCreatingFilmWithOldReleaseDate() { - ValidationException exception = assertThrows(ValidationException.class, () -> { - filmController.createFilm(invalidFilmReleaseDate); - }); - assertNotNull(exception); - assertEquals("Film release date cannot be earlier than December 28, 1895", exception.getMessage()); - } - - @Test - public void shouldThrowExceptionWhenUpdatingFilmWithWrongId() { - NotFoundException exception = assertThrows(NotFoundException.class, () -> { - filmController.update(filmWithWrongId); - }); - assertNotNull(exception); - } - - @AfterAll - public static void shouldReturnAllFilms() { - assertNotNull(filmController.getFilms()); - } -} \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/FilmDbStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/FilmDbStorageTest.java new file mode 100644 index 0000000..32fff06 --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/FilmDbStorageTest.java @@ -0,0 +1,105 @@ +package ru.yandex.practicum.filmorate; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.jdbc.core.JdbcTemplate; +import ru.yandex.practicum.filmorate.exception.ConditionsNotMetException; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.model.Buffer; +import ru.yandex.practicum.filmorate.storage.film.FilmDbStorage; + +import java.time.LocalDate; +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +class FilmDbStorageTest { + + @Mock + private JdbcTemplate jdbcTemplate; + + private FilmDbStorage filmDbStorage; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + filmDbStorage = new FilmDbStorage(jdbcTemplate); + } + + @Test + void createFilmWithEmptyNameShouldThrowException() { + Buffer buffer = Buffer.of( + null, // id + "", // Пустое название + "Valid description", + LocalDate.of(2000, 1, 1), + 120, + Collections.singletonList("1"), + 1L + ); + + assertThrows(ConditionsNotMetException.class, () -> filmDbStorage.create(buffer)); + } + + @Test + void createFilmWithLongDescriptionShouldThrowException() { + Buffer buffer = Buffer.of( + null, // id + "Valid Name", + "A".repeat(201), // Описание длиннее 200 символов + LocalDate.of(2000, 1, 1), + 120, + Collections.singletonList("1"), + 1L + ); + + + assertThrows(ConditionsNotMetException.class, () -> filmDbStorage.create(buffer)); + } + + @Test + void createFilmWithInvalidReleaseDateShouldThrowException() { + Buffer buffer = Buffer.of( + null, // id + "Valid Name", + "Valid description", + LocalDate.of(1890, 1, 1), // Дата раньше 28 декабря 1895 года + 120, + Collections.singletonList("1"), + 1L + ); + assertThrows(ConditionsNotMetException.class, () -> filmDbStorage.create(buffer)); + } + + @Test + void createFilmWithNegativeDurationShouldThrowException() { + Buffer buffer = Buffer.of( + null, // id + "Valid Name", + "Valid description", + LocalDate.of(2000, 1, 1), + -120, // Отрицательная продолжительность + Collections.singletonList("1"), + 1L + ); + + assertThrows(ConditionsNotMetException.class, () -> filmDbStorage.create(buffer)); + } + + @Test + void createFilmWithInvalidMpaShouldThrowException() { + Buffer buffer = Buffer.of( + null, // id + "Valid Name", + "Valid description", + LocalDate.of(2000, 1, 1), + 120, + Collections.singletonList("1"), + 0L // Некорректный рейтинг MPA + ); + + assertThrows(NotFoundException.class, () -> filmDbStorage.create(buffer)); + } +} \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/FilmoRateApplicationTests.java b/src/test/java/ru/yandex/practicum/filmorate/FilmoRateApplicationTests.java new file mode 100644 index 0000000..92d1760 --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/FilmoRateApplicationTests.java @@ -0,0 +1,54 @@ +package ru.yandex.practicum.filmorate; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.model.User; +import ru.yandex.practicum.filmorate.storage.user.UserDbStorage; + +import java.time.LocalDate; +import java.util.HashSet; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(SpringRunner.class) +@SpringBootTest +@AutoConfigureTestDatabase +public class FilmoRateApplicationTests { + + @Autowired + private UserDbStorage userDbStorage; + + @Test + public void testFindUserById() throws NotFoundException { + // Создаем пользователя через метод of + User user = User.of( + null, // ID будет сгенерирован базой данных + "Test User", + "test@example.com", + "testLogin", + LocalDate.of(1990, 1, 1), + new HashSet<>(), + new HashSet<>() + ); + + // Добавляем пользователя в базу данных + User createdUser = userDbStorage.create(user); + + // Проверяем, что пользователь с ID = createdUser.getId() существует в базе данных + User foundUser = userDbStorage.findById(createdUser.getId()); + + // Проверяем, что найденный пользователь соответствует ожидаемым значениям + assertThat(foundUser) + .isNotNull() + .hasFieldOrPropertyWithValue("id", createdUser.getId()) + .hasFieldOrPropertyWithValue("email", "test@example.com") + .hasFieldOrPropertyWithValue("login", "testLogin") + .hasFieldOrPropertyWithValue("name", "Test User") + .hasFieldOrPropertyWithValue("birthday", LocalDate.of(1990, 1, 1)); + } +} \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/InMemoryUserStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/InMemoryUserStorageTest.java deleted file mode 100644 index 9365e5c..0000000 --- a/src/test/java/ru/yandex/practicum/filmorate/InMemoryUserStorageTest.java +++ /dev/null @@ -1,95 +0,0 @@ -package ru.yandex.practicum.filmorate; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import ru.yandex.practicum.filmorate.exception.NotFoundException; -import ru.yandex.practicum.filmorate.model.User; -import ru.yandex.practicum.filmorate.storage.user.InMemoryUserStorage; - -import java.time.LocalDate; -import java.util.HashSet; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; - -public class InMemoryUserStorageTest { - - private InMemoryUserStorage userStorage; - - @BeforeEach - public void setUp() { - userStorage = new InMemoryUserStorage(); - } - - @Test - public void shouldAddFriendAndUpdateBothUsers() throws Exception { - User user1 = User.of(null, "User 1", "user1@example.com", "user1", LocalDate.of(1990, 1, 1), new HashSet<>()); - User user2 = User.of(null, "User 2", "user2@example.com", "user2", LocalDate.of(1995, 5, 5), new HashSet<>()); - - userStorage.create(user1); - userStorage.create(user2); - - userStorage.addFriend(user1.getId(), user2.getId()); - - List friendsOfUser1 = (List) userStorage.getFriends(user1.getId()); - assertEquals(1, friendsOfUser1.size()); - assertEquals(user2.getId(), friendsOfUser1.get(0).getId()); - - List friendsOfUser2 = (List) userStorage.getFriends(user2.getId()); - assertEquals(1, friendsOfUser2.size()); - assertEquals(user1.getId(), friendsOfUser2.get(0).getId()); - } - - @Test - public void shouldRemoveFriendAndUpdateBothUsers() throws Exception { - User user1 = User.of(null, "User 1", "user1@example.com", "user1", LocalDate.of(1990, 1, 1), null); - User user2 = User.of(null, "User 2", "user2@example.com", "user2", LocalDate.of(1995, 5, 5), null); - - userStorage.create(user1); - userStorage.create(user2); - - userStorage.addFriend(user1.getId(), user2.getId()); - - userStorage.removeFriend(user1.getId(), user2.getId()); - - assertTrue(((List) userStorage.getFriends(user1.getId())).isEmpty()); - assertTrue(((List) userStorage.getFriends(user2.getId())).isEmpty()); - } - - @Test - public void shouldGetCommonFriends() throws Exception { - User user1 = User.of(null, "User 1", "user1@example.com", "user1", LocalDate.of(1990, 1, 1), new HashSet<>()); - User user2 = User.of(null, "User 2", "user2@example.com", "user2", LocalDate.of(1995, 5, 5), new HashSet<>()); - User user3 = User.of(null, "User 3", "user3@example.com", "user3", LocalDate.of(2000, 1, 1), new HashSet<>()); - - userStorage.create(user1); - userStorage.create(user2); - userStorage.create(user3); - - userStorage.addFriend(user1.getId(), user3.getId()); - userStorage.addFriend(user2.getId(), user3.getId()); - - List commonFriends = (List) userStorage.getCommonFriends(user1.getId(), user2.getId()); - - assertEquals(1, commonFriends.size()); - assertEquals(user3.getId(), commonFriends.get(0).getId()); - } - - @Test - public void shouldThrowNotFoundExceptionForUnknownUser() { - User user1 = User.of(null, "User 1", "user1@example.com", "user1", LocalDate.of(1990, 1, 1), new HashSet<>()); - userStorage.create(user1); - - NotFoundException exception = assertThrows(NotFoundException.class, () -> { - userStorage.addFriend(user1.getId(), 999L); - }); - assertNotNull(exception); - assertTrue(exception.getMessage().contains("not found")); - - exception = assertThrows(NotFoundException.class, () -> { - userStorage.removeFriend(user1.getId(), 999L); - }); - assertNotNull(exception); - assertTrue(exception.getMessage().contains("not found")); - } -} \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/UserControllerTest.java b/src/test/java/ru/yandex/practicum/filmorate/UserControllerTest.java deleted file mode 100644 index fa1fbe0..0000000 --- a/src/test/java/ru/yandex/practicum/filmorate/UserControllerTest.java +++ /dev/null @@ -1,100 +0,0 @@ -package ru.yandex.practicum.filmorate; - -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import ru.yandex.practicum.filmorate.exception.ValidationException; -import ru.yandex.practicum.filmorate.exception.DuplicatedDataException; -import ru.yandex.practicum.filmorate.exception.NotFoundException; -import ru.yandex.practicum.filmorate.model.User; -import ru.yandex.practicum.filmorate.service.UserService; -import ru.yandex.practicum.filmorate.controller.UserController; -import ru.yandex.practicum.filmorate.storage.user.InMemoryUserStorage; -import ru.yandex.practicum.filmorate.storage.user.UserStorage; - -import java.time.LocalDate; -import java.time.format.DateTimeFormatter; -import java.util.HashSet; - -import static org.junit.jupiter.api.Assertions.*; - -public class UserControllerTest { - - private static UserController userController; - private static User validUser; - private static User invalidEmailUser; - private static User duplicateEmailUser; - private static User invalidLoginUser; - private static User userWithNoId; - private static User userWithWrongId; - - @BeforeAll - public static void start() throws ValidationException, DuplicatedDataException { - UserStorage userStorage = new InMemoryUserStorage(); - userController = new UserController(new UserService(userStorage)); - - // Валидный пользователь - validUser = User.of(0L, "Valid User", "valid@mail.ru", "validLogin", LocalDate.parse("2000-01-01", DateTimeFormatter.ofPattern("yyyy-MM-dd")), new HashSet<>()); - userController.create(validUser); - - // Пользователь с некорректным email - invalidEmailUser = User.of(0L, "Invalid Email User", "invalidEmail", "invalidLogin", LocalDate.parse("2000-01-01", DateTimeFormatter.ofPattern("yyyy-MM-dd")), new HashSet<>()); - - // Пользователь с дублирующимся email - duplicateEmailUser = User.of(0L, "Duplicate Email User", "valid@mail.ru", "duplicateLogin", LocalDate.parse("2000-01-01", DateTimeFormatter.ofPattern("yyyy-MM-dd")), new HashSet<>()); - - // Пользователь с некорректным логином (с пробелами) - invalidLoginUser = User.of(0L, "Invalid Login User", "login@mail.ru", "invalid login", LocalDate.parse("2000-01-01", DateTimeFormatter.ofPattern("yyyy-MM-dd")), new HashSet<>()); - - // Пользователь без ID - userWithNoId = User.of(null, "No ID User", "noid@mail.ru", "noIdLogin", LocalDate.parse("2000-01-01", DateTimeFormatter.ofPattern("yyyy-MM-dd")), new HashSet<>()); - - // Пользователь с несуществующим ID - userWithWrongId = User.of(999L, "Wrong ID User", "wrongid@mail.ru", "wrongIdLogin", LocalDate.parse("2000-01-01", DateTimeFormatter.ofPattern("yyyy-MM-dd")), new HashSet<>()); - } - - @Test - public void shouldThrowExceptionWhenCreatingUserWithInvalidEmail() { - ValidationException exception = assertThrows(ValidationException.class, () -> { - userController.create(invalidEmailUser); - }); - assertNotNull(exception); - assertEquals("Invalid email", exception.getMessage()); - } - - @Test - public void shouldThrowExceptionWhenCreatingUserWithDuplicateEmail() { - DuplicatedDataException exception = assertThrows(DuplicatedDataException.class, () -> { - userController.create(duplicateEmailUser); - }); - assertNotNull(exception); - assertEquals("A user with this email already exists", exception.getMessage()); - } - - @Test - public void shouldThrowExceptionWhenCreatingUserWithInvalidLogin() { - ValidationException exception = assertThrows(ValidationException.class, () -> { - userController.create(invalidLoginUser); - }); - assertNotNull(exception); - assertEquals("Login cannot be empty or contain spaces", exception.getMessage()); - } - - - @Test - public void shouldThrowExceptionWhenUpdatingUserWithNoId() { - ValidationException exception = assertThrows(ValidationException.class, () -> { - userController.update(userWithNoId); - }); - assertNotNull(exception); - assertEquals("User ID cannot be null", exception.getMessage()); - } - - @Test - public void shouldThrowExceptionWhenUpdatingUserWithWrongId() { - NotFoundException exception = assertThrows(NotFoundException.class, () -> { - userController.update(userWithWrongId); - }); - assertNotNull(exception); - assertEquals("User with ID = 999 not found", exception.getMessage()); - } -} \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/UserDbStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/UserDbStorageTest.java new file mode 100644 index 0000000..d417fad --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/UserDbStorageTest.java @@ -0,0 +1,112 @@ +package ru.yandex.practicum.filmorate; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.context.jdbc.Sql; +import ru.yandex.practicum.filmorate.exception.DuplicatedDataException; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.exception.ValidationException; +import ru.yandex.practicum.filmorate.model.User; +import ru.yandex.practicum.filmorate.storage.user.UserDbStorage; + +import java.time.LocalDate; +import java.util.HashSet; + +import static org.junit.jupiter.api.Assertions.*; + +@JdbcTest +@Sql(scripts = {"/schema.sql", "/data.sql"}) // Загружаем схему и тестовые данные +class UserDbStorageTest { + + @Autowired + private JdbcTemplate jdbcTemplate; + + private UserDbStorage userDbStorage; + + @BeforeEach + void setUp() { + userDbStorage = new UserDbStorage(jdbcTemplate); + } + + @Test + void testCreateUser() throws ValidationException, DuplicatedDataException { + // Создаем нового пользователя с использованием метода User.of(...) + User user = User.of( + null, + "Test User", + "test@example.com", + "testLogin", + LocalDate.of(1990, 1, 1), + new HashSet<>(), // Инициализируем пустое множество friends + new HashSet<>() // Инициализируем пустое множество friendRequests + ); + + User createdUser = userDbStorage.create(user); + + assertNotNull(createdUser.getId()); + assertEquals("test@example.com", createdUser.getEmail()); + assertEquals("testLogin", createdUser.getLogin()); + assertEquals("Test User", createdUser.getName()); + assertEquals(LocalDate.of(1990, 1, 1), createdUser.getBirthday()); + } + + @Test + void testUpdateUser() throws ValidationException, DuplicatedDataException, NotFoundException { + // Создаем нового пользователя с использованием метода User.of(...) + User user = User.of( + null, + "Test User", + "test@example.com", + "testLogin", + LocalDate.of(1990, 1, 1), + new HashSet<>(), + new HashSet<>() + ); + User createdUser = userDbStorage.create(user); + + // Обновляем пользователя с использованием метода User.of(...) + User updatedUser = User.of( + createdUser.getId(), + "Updated User", + "test@example.com", + "testLogin", + LocalDate.of(1990, 1, 1), + new HashSet<>(), + new HashSet<>() + ); + User result = userDbStorage.update(updatedUser); + + assertEquals("Updated User", result.getName()); + } + + @Test + void testFindById() throws ValidationException, DuplicatedDataException, NotFoundException { + // Создаем нового пользователя с использованием метода User.of(...) + User user = User.of( + null, + "Test User", + "test@example.com", + "testLogin", + LocalDate.of(1990, 1, 1), + new HashSet<>(), + new HashSet<>() + ); + User createdUser = userDbStorage.create(user); + + User foundUser = userDbStorage.findById(createdUser.getId()); + + assertEquals(createdUser.getId(), foundUser.getId()); + assertEquals("test@example.com", foundUser.getEmail()); + assertEquals("testLogin", foundUser.getLogin()); + assertEquals("Test User", foundUser.getName()); + assertEquals(LocalDate.of(1990, 1, 1), foundUser.getBirthday()); + } + + @Test + void testFindByIdNotFound() { + assertThrows(NotFoundException.class, () -> userDbStorage.findById(999L)); + } +} \ No newline at end of file