diff --git a/ExamplesOfRequests.png b/ExamplesOfRequests.png index 7f30c7d..ae48855 100644 Binary files a/ExamplesOfRequests.png and b/ExamplesOfRequests.png differ diff --git a/FILMO_RATE_DATABASE_MAP.png b/FILMO_RATE_DATABASE_MAP.png new file mode 100644 index 0000000..b50dbc3 Binary files /dev/null and b/FILMO_RATE_DATABASE_MAP.png differ diff --git a/QuickDBD-FILMO_RATE.png b/QuickDBD-FILMO_RATE.png deleted file mode 100644 index 34e7885..0000000 Binary files a/QuickDBD-FILMO_RATE.png and /dev/null differ diff --git a/README.md b/README.md index 3488306..7a8723d 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,21 @@ -# java-filmorate (Java Filmorate) -### Описание (Description) -The Filmorate project — это приложение для работы с базой данных фильмов. +# java-filmorate +The Filmorate project — это приложение для работы с базой данных фильмов. +А также площадка для общения и взаимодействия пользователей. + ### Основные возможности -- Хранение данных о фильмах +- Хранение данных о фильмах - Хранение данных о пользователях - Возможность пользователям ставить лайки фильмам - Возможность пользователям добавлять друг друга в друзья - Возможность получать рейтинг TOP_N фильмов по количеству лайков +- Возможность получать общих с другим пользователем фильмов с сортировкой по их популярности +- Возможность заводить друзей +- Возможность оставлять отзывы о фильмах - Фильмы содержат возрастные ограничения по стандарту MPPA +- Реализован поиск по названию и режиссерам - Реализована система жанров для фильмов +- Есть лента событий + ### Требования (Environmental Requirements) Для запуска проекта требуется: - Java версии 21 Приложение работает на порту 8080 @@ -48,7 +55,7 @@ The Filmorate project — это приложение для работы с б - logback-classic - logbook-spring-boot-starter ### Схема базы данных (Database map) -![DatabaseMap](QuickDBD-FILMO_RATE.png "Database map:") +![DatabaseMap](FILMO_RATE_DATABASE_MAP.png "Database map:") ### Примеры запросов к базе данных: 1. Get TOP-10 films: ```sql diff --git a/src/main/java/ru/yandex/practicum/filmorate/FilmorateApplication.java b/src/main/java/ru/yandex/practicum/filmorate/FilmorateApplication.java index 3f2baca..32a2e5f 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/FilmorateApplication.java +++ b/src/main/java/ru/yandex/practicum/filmorate/FilmorateApplication.java @@ -2,10 +2,11 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; -@SpringBootApplication +@SpringBootApplication(exclude = {HibernateJpaAutoConfiguration.class}) public class FilmorateApplication { - public static void main(String[] args) { - SpringApplication.run(FilmorateApplication.class, args); - } + public static void main(String[] args) { + SpringApplication.run(FilmorateApplication.class, args); + } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/DirectorController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/DirectorController.java new file mode 100644 index 0000000..8e7ea6b --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/DirectorController.java @@ -0,0 +1,43 @@ +package ru.yandex.practicum.filmorate.controller; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import ru.yandex.practicum.filmorate.dto.DirectorDto; +import ru.yandex.practicum.filmorate.dto.request.create.DirectorCreateRequest; +import ru.yandex.practicum.filmorate.dto.request.update.DirectorUpdateRequest; +import ru.yandex.practicum.filmorate.service.DirectorService; + +import java.util.Collection; + +@RequestMapping("/directors") +@RestController +@RequiredArgsConstructor +public class DirectorController { + private final DirectorService directorService; + + @GetMapping + public Collection getAllDirectors() { + return directorService.getAllDirectorDtos(); + } + + @GetMapping("/{directorId}") + public DirectorDto getDirectorById(@PathVariable Integer directorId) { + return directorService.getDirectorDtoById(directorId); + } + + @PostMapping + public DirectorDto createDirector(@RequestBody @Valid DirectorCreateRequest directorRequest) { + return directorService.create(directorRequest); + } + + @PutMapping + public DirectorDto updateDirector(@RequestBody @Valid DirectorUpdateRequest directorRequest) { + return directorService.update(directorRequest); + } + + @DeleteMapping("/{directorId}") + public void deleteDirector(@PathVariable Integer directorId) { + directorService.delete(directorId); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/ErrorHandler.java b/src/main/java/ru/yandex/practicum/filmorate/controller/ErrorHandler.java index e4daae7..0a33078 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/ErrorHandler.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/ErrorHandler.java @@ -4,14 +4,15 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.validation.FieldError; import org.springframework.validation.method.MethodValidationException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; -import ru.yandex.practicum.filmorate.model.ErrorResponse; import ru.yandex.practicum.filmorate.exception.*; +import ru.yandex.practicum.filmorate.model.ErrorResponse; import java.util.HashMap; import java.util.Map; @@ -83,6 +84,12 @@ public ErrorResponse handlerMethodValidationException(final MethodValidationExce return new ErrorResponse(e.getAllErrors().getFirst().getDefaultMessage()); } + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler + public ErrorResponse handleEmptyBody(HttpMessageNotReadableException ex) { + return new ErrorResponse("Тело запроса отсутствует или некорректно"); + } + @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity> handleMethodArgumentException(final MethodArgumentNotValidException e) { Map errors = new HashMap<>(); 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 90bb273..2362097 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java @@ -4,54 +4,80 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; -import ru.yandex.practicum.filmorate.dto.request.create.FilmCreateRequest; import ru.yandex.practicum.filmorate.dto.FilmDto; +import ru.yandex.practicum.filmorate.dto.request.create.FilmCreateRequest; import ru.yandex.practicum.filmorate.dto.request.update.FilmUpdateRequest; -import ru.yandex.practicum.filmorate.servise.FilmService; -import ru.yandex.practicum.filmorate.servise.util.LikeAction; +import ru.yandex.practicum.filmorate.service.FilmService; +import ru.yandex.practicum.filmorate.service.util.LikeAction; import java.util.Collection; +@RequestMapping("/films") @RestController @RequiredArgsConstructor -@RequestMapping("/films") public class FilmController { - private final FilmService filmService; - - @GetMapping - public Collection findAll() { - return filmService.findAll(); - } - - @GetMapping("/{id}") - public FilmDto findById(@PathVariable long id) { - return filmService.findById(id); - } - - @GetMapping("/popular") - public Collection getTop(@RequestParam (defaultValue = "10", required = false) Integer count) { - return filmService.getTopFilms(count); - } - - @PostMapping - @ResponseStatus(HttpStatus.CREATED) - public FilmDto create(@RequestBody @Valid FilmCreateRequest request) { - return filmService.create(request); - } - - @PutMapping - public FilmDto update(@RequestBody FilmUpdateRequest request) { - return filmService.update(request); - } - - @PutMapping("/{filmId}/like/{userId}") - public void setLike(@PathVariable long filmId, @PathVariable long userId) { - filmService.changeLike(LikeAction.SET, filmId, userId); - } - - @DeleteMapping("/{filmId}/like/{userId}") - public void deleteLike(@PathVariable long filmId, @PathVariable long userId) { - filmService.changeLike(LikeAction.REMOVE, filmId, userId); - } -} + private final FilmService filmService; + + @GetMapping + public Collection findAll() { + return filmService.findAll(); + } + + @GetMapping("/{id}") + public FilmDto findById(@PathVariable long id) { + return filmService.findById(id); + } + + @GetMapping("/popular") + public Collection getTop(@RequestParam(defaultValue = "10", required = false) Integer count, + @RequestParam(required = false) Integer genreId, + @RequestParam(required = false) String year) { + return filmService.getTopFilms(count, genreId, year); + } + + @GetMapping("/common") + public Collection getCommonLikedFilms(@RequestParam(required = true) Long userId, + @RequestParam(required = true) Long friendId) { + return filmService.getCommonLikedFilms(userId, friendId); + } + + @GetMapping("/director/{directorId}") + public Collection findSortedFilmsOfDirector( + @PathVariable Integer directorId, + @RequestParam(required = false) String sortBy + ) { + return filmService.getSortedFilmsOfDirector(directorId, sortBy); + } + + @GetMapping("/search") + public Collection search(@RequestParam String query, @RequestParam String by) { + return filmService.search(query, by); + } + + @ResponseStatus(HttpStatus.CREATED) + @PostMapping + public FilmDto create(@RequestBody @Valid FilmCreateRequest request) { + return filmService.create(request); + } + + @PutMapping + public FilmDto update(@RequestBody FilmUpdateRequest request) { + return filmService.update(request); + } + + @PutMapping("/{filmId}/like/{userId}") + public void setLike(@PathVariable long filmId, @PathVariable long userId) { + filmService.changeLike(LikeAction.SET, filmId, userId); + } + + @DeleteMapping("/{filmId}/like/{userId}") + public void deleteLike(@PathVariable long filmId, @PathVariable long userId) { + filmService.changeLike(LikeAction.REMOVE, filmId, userId); + } + + @DeleteMapping("/{filmId}") + public void deleteFilm(@PathVariable long filmId) { + filmService.remove(filmId); + } +} \ 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 index aa7a4eb..6c8bf7c 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/GenreController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/GenreController.java @@ -6,12 +6,12 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import ru.yandex.practicum.filmorate.dto.GenreDto; -import ru.yandex.practicum.filmorate.servise.GenreService; +import ru.yandex.practicum.filmorate.service.GenreService; import java.util.Collection; -@RestController @RequestMapping("/genres") +@RestController @RequiredArgsConstructor public class GenreController { private final GenreService genreService; diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/HomeController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/HomeController.java index 8af1dcf..4e16f33 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/HomeController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/HomeController.java @@ -4,43 +4,43 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -@RestController @RequestMapping("") +@RestController public class HomeController { - @GetMapping - public String homePage() { - return """ -


-
-                    ███████╗██╗██╗     ███╗   ███╗ ██████╗     ██████╗  █████╗ ████████╗███████╗
-                    ██╔════╝██║██║     ████╗ ████║██╔═══██╗    ██╔══██╗██╔══██╗╚══██╔══╝██╔════╝
-                    █████╗  ██║██║     ██╔████╔██║██║   ██║    ██████╔╝███████║   ██║   █████╗
-                    ██╔══╝  ██║██║     ██║╚██╔╝██║██║   ██║    ██╔══██╗██╔══██║   ██║   ██╔══╝
-                    ██║     ██║███████╗██║ ╚═╝ ██║╚██████╔╝    ██║  ██║██║  ██║   ██║   ███████╗
-                    ╚═╝     ╚═╝╚══════╝╚═╝     ╚═╝ ╚═════╝     ╚═╝  ╚═╝╚═╝  ╚═╝   ╚═╝   ╚══════╝
-                
- """; - } + @GetMapping + public String homePage() { + return """ +


+
+				    ███████╗██╗██╗     ███╗   ███╗ ██████╗     ██████╗  █████╗ ████████╗███████╗
+				    ██╔════╝██║██║     ████╗ ████║██╔═══██╗    ██╔══██╗██╔══██╗╚══██╔══╝██╔════╝
+				    █████╗  ██║██║     ██╔████╔██║██║   ██║    ██████╔╝███████║   ██║   █████╗
+				    ██╔══╝  ██║██║     ██║╚██╔╝██║██║   ██║    ██╔══██╗██╔══██║   ██║   ██╔══╝
+				    ██║     ██║███████╗██║ ╚═╝ ██║╚██████╔╝    ██║  ██║██║  ██║   ██║   ███████╗
+				    ╚═╝     ╚═╝╚══════╝╚═╝     ╚═╝ ╚═════╝     ╚═╝  ╚═╝╚═╝  ╚═╝   ╚═╝   ╚══════╝
+				
+ """; + } - @GetMapping("favicon.ico") - public String faviconIco() { - return """ -


- """; - } + @GetMapping("favicon.ico") + public String faviconIco() { + return """ +


+ """; + } - @GetMapping(".well-known/appspecific/com.chrome.devtools.json") - public String devtools() { - return """ -


- """; - } + @GetMapping(".well-known/appspecific/com.chrome.devtools.json") + public String devtools() { + return """ +


+ """; + } - @GetMapping("sw.js") - public String swjs() { - return """ -


- """; - } + @GetMapping("sw.js") + public String swjs() { + return """ +


+ """; + } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/MpaController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/MpaController.java index 19eb0f0..8e4bd37 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/MpaController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/MpaController.java @@ -6,15 +6,15 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import ru.yandex.practicum.filmorate.dto.MpaDto; -import ru.yandex.practicum.filmorate.servise.MpaServise; +import ru.yandex.practicum.filmorate.service.MpaService; import java.util.Collection; -@RestController @RequestMapping("/mpa") +@RestController @RequiredArgsConstructor public class MpaController { - private final MpaServise mpaService; + private final MpaService mpaService; @GetMapping("/{mpaId}") public MpaDto getMpaById(@PathVariable long mpaId) { diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/ReviewController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/ReviewController.java new file mode 100644 index 0000000..e14a105 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/ReviewController.java @@ -0,0 +1,69 @@ +package ru.yandex.practicum.filmorate.controller; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import ru.yandex.practicum.filmorate.dto.ReviewDto; +import ru.yandex.practicum.filmorate.dto.request.create.ReviewCreateRequest; +import ru.yandex.practicum.filmorate.dto.request.update.ReviewUpdateRequest; +import ru.yandex.practicum.filmorate.service.ReviewService; + +import java.util.Collection; + +@RequestMapping("/reviews") +@RestController +@RequiredArgsConstructor +public class ReviewController { + + private final ReviewService reviewService; + + @GetMapping("/{id}") + public ReviewDto findById(@PathVariable Long id) { + return reviewService.findReviewById(id); + } + + @GetMapping + public Collection findByFilmId( + @RequestParam(required = false) Long filmId, + @RequestParam(defaultValue = "10") int count + ) { + return reviewService.findReviewsByFilmId(filmId, count); + } + + @ResponseStatus(HttpStatus.CREATED) + @PostMapping + public ReviewDto create(@RequestBody @Valid ReviewCreateRequest request) { + return reviewService.createReview(request); + } + + @PutMapping + public ReviewDto update(@RequestBody @Valid ReviewUpdateRequest request) { + return reviewService.updateReview(request); + } + + @PutMapping("/{id}/like/{userId}") + public void addLike(@PathVariable Long id, @PathVariable Long userId) { + reviewService.addReviewLike(id, userId); + } + + @PutMapping("/{id}/dislike/{userId}") + public void addDislike(@PathVariable Long id, @PathVariable Long userId) { + reviewService.addReviewDislike(id, userId); + } + + @DeleteMapping("/{id}") + public void delete(@PathVariable Long id) { + reviewService.deleteReview(id); + } + + @DeleteMapping("/{id}/like/{userId}") + public void removeLike(@PathVariable Long id, @PathVariable Long userId) { + reviewService.removeReviewLike(id, userId); + } + + @DeleteMapping("/{id}/dislike/{userId}") + public void removeDislike(@PathVariable Long id, @PathVariable Long userId) { + reviewService.removeReviewDislike(id, userId); + } +} 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 0e1498a..bf7326c 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java @@ -4,11 +4,13 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; +import ru.yandex.practicum.filmorate.dto.EventDto; +import ru.yandex.practicum.filmorate.dto.FilmDto; +import ru.yandex.practicum.filmorate.dto.UserDto; import ru.yandex.practicum.filmorate.dto.request.create.UserCreateRequest; import ru.yandex.practicum.filmorate.dto.request.update.UserUpdateRequest; -import ru.yandex.practicum.filmorate.dto.UserDto; -import ru.yandex.practicum.filmorate.servise.util.FriendsAction; -import ru.yandex.practicum.filmorate.servise.UserService; +import ru.yandex.practicum.filmorate.service.UserService; +import ru.yandex.practicum.filmorate.service.util.FriendsAction; import java.util.Collection; @@ -39,6 +41,16 @@ public Collection getMutualFriends(@PathVariable(name = "id") long user return userService.getListOfMutualFriends(userId, otherId); } + @GetMapping("/{id}/feed") + public Collection getUserEvents(@PathVariable(name = "id") long userId) { + return userService.getUserEvents(userId); + } + + @GetMapping("/{id}/recommendations") + public Collection getRecommendations(@PathVariable long id) { + return userService.getRecommendations(id); + } + @PostMapping @ResponseStatus(HttpStatus.CREATED) public UserDto create(@RequestBody @Valid UserCreateRequest userRequest) { @@ -59,4 +71,9 @@ public void addToFriends(@PathVariable long userId, @PathVariable long friendId) public void removeFromFriends(@PathVariable long userId, @PathVariable long friendId) { userService.changeFriends(FriendsAction.REMOVE, userId, friendId); } + + @DeleteMapping("/{userId}") + public void deleteUser(@PathVariable long userId) { + userService.remove(userId); + } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/DirectorStorage.java b/src/main/java/ru/yandex/practicum/filmorate/dal/DirectorStorage.java new file mode 100644 index 0000000..4cbedc8 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/DirectorStorage.java @@ -0,0 +1,28 @@ +package ru.yandex.practicum.filmorate.dal; + +import ru.yandex.practicum.filmorate.model.Director; + +import java.util.Collection; +import java.util.Optional; + +public interface DirectorStorage { + + /// Получить режиссера по id + Optional findById(long id); + + /// Получить всех режиссеров + Collection findAll(); + + /// Добавить нового режиссера + Director createDirector(Director director); + + /// Обновить данные режиссера + Director updateDirector(Director director); + + /// Удалить режиссера по id + void deleteDirector(Integer directorId); + + /// Проверить что директор отсутствует по id + boolean checkDirectorIsNotPresent(int directorId); + +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/EventStorage.java b/src/main/java/ru/yandex/practicum/filmorate/dal/EventStorage.java new file mode 100644 index 0000000..e772f09 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/EventStorage.java @@ -0,0 +1,14 @@ +package ru.yandex.practicum.filmorate.dal; + +import ru.yandex.practicum.filmorate.model.Event; + +import java.util.Collection; + +public interface EventStorage { + + /// Получение всех действий пользователя по его id + Collection getAllEventsByUserId(long userId); + + /// Добавление события + Event addEvent(Event event); +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/FilmStorage.java b/src/main/java/ru/yandex/practicum/filmorate/dal/FilmStorage.java index 6927ef8..1fe9ca9 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dal/FilmStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/FilmStorage.java @@ -3,7 +3,9 @@ import ru.yandex.practicum.filmorate.model.Film; import java.util.Collection; +import java.util.Map; import java.util.Optional; +import java.util.Set; public interface FilmStorage { @@ -25,9 +27,29 @@ public interface FilmStorage { /// Получить топ фильмов Collection getTop(int count); + /// Получить топ фильмов по фильтрам + Collection getTopByFilters(Integer count, Integer genreId, String year); + /// Поставить лайк void setLike(long filmId, long userId); /// Убрать лайк void removeLike(long filmId, long userId); + + /// Получить список фильмов по id режиссера + Collection getALLFilmsOfDirector(Integer directorId); + + /// Метод для получения матрицы лайков всех пользователей + Map> getAllLikes(); + + /// Получить список филиьов, которые понравились обоим пользователям + Collection getCommonLikedFilms(long userId, long friendId); + + /// Поиск фильмов по названию и описанию + Collection search(String query, String by); + + /// Удаление фильма по id + void removeFilm(long filmId); + + Collection getFilmsByIds(Collection ids); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/GenreStorage.java b/src/main/java/ru/yandex/practicum/filmorate/dal/GenreStorage.java index 447b7e4..d54ad49 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dal/GenreStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/GenreStorage.java @@ -7,9 +7,13 @@ import java.util.Optional; public interface GenreStorage { + + /// Получить жанр по id Optional getGenreById(long genreId); + /// Получить список всех жанров Collection getAllGenres(); + /// Получить матрици всех жанров с их id Map getMapOfAllGenres(); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/MpaStorage.java b/src/main/java/ru/yandex/practicum/filmorate/dal/MpaStorage.java index c7a14e4..872f3e0 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dal/MpaStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/MpaStorage.java @@ -7,9 +7,13 @@ import java.util.Optional; public interface MpaStorage { + + /// Получить МППА рейтинг по id Optional getMpaById(long mpaId); + /// Получить список всех МППА рейтингов Collection getAllMpa(); + /// Получить матрицу МППА рейтингов и их id Map getMapOfAllMpa(); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/ReviewStorage.java b/src/main/java/ru/yandex/practicum/filmorate/dal/ReviewStorage.java new file mode 100644 index 0000000..6db0364 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/ReviewStorage.java @@ -0,0 +1,32 @@ +package ru.yandex.practicum.filmorate.dal; + +import ru.yandex.practicum.filmorate.model.Review; + +import java.util.Collection; +import java.util.Optional; + +public interface ReviewStorage { + + Review createReview(Review review); + + Review updateReview(Review review); + + void deleteReview(long id); + + Optional findReviewById(long id); + + Collection findReviewsByFilmId(Long filmId, int count); + + void addReviewLike(long id, long userId); + + void addReviewDislike(long id, long userId); + + void removeReviewLike(long id, long userId); + + void removeReviewDislike(long id, long userId); + + boolean checkReviewIsNotPresent(long id); + + Optional getReactionsStatus(long reviewId, long userId); +} + diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/UserStorage.java b/src/main/java/ru/yandex/practicum/filmorate/dal/UserStorage.java index 368853b..65ff810 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dal/UserStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/UserStorage.java @@ -36,4 +36,7 @@ public interface UserStorage { /// Получить список id друзей Collection getFriendIds(long userId); + + /// Удаление пользователя по id + void removeUser(long userId); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/database/BaseDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/dal/database/BaseDbStorage.java index 05d792d..209d5cb 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dal/database/BaseDbStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/database/BaseDbStorage.java @@ -1,6 +1,7 @@ package ru.yandex.practicum.filmorate.dal.database; import lombok.RequiredArgsConstructor; +import org.springframework.dao.DataRetrievalFailureException; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; @@ -13,57 +14,70 @@ @RequiredArgsConstructor public class BaseDbStorage { - protected final JdbcTemplate jdbc; - protected final RowMapper rowMapper; + protected final JdbcTemplate jdbc; + protected final RowMapper rowMapper; - protected Set findColumnByQuery(String query, Class objectClass, Object... params) { - try { - return new HashSet<>(jdbc.queryForList(query, objectClass, params)); - } catch (EmptyResultDataAccessException ignored) { - return HashSet.newHashSet(0); - } - } + protected Set findColumnByQuery(String query, Class objectClass, Object... params) { + try { + return new HashSet<>(jdbc.queryForList(query, objectClass, params)); + } catch (EmptyResultDataAccessException ignored) { + return HashSet.newHashSet(0); + } + } - protected Optional findOneByIdInTable(long id, String tableName) { - return jdbc.query("SELECT * FROM " + tableName + " WHERE id = ?", rowMapper, id) - .stream() - .findFirst(); - } + protected Optional findOneByIdInTable(long id, String tableName) { + return jdbc.query("SELECT * FROM " + tableName + " WHERE id = ?", rowMapper, id) + .stream() + .findFirst(); + } - protected Collection findAllInTable(String tableName) { - return jdbc.query("SELECT * FROM " + tableName + " ORDER BY id", rowMapper) - .stream() - .toList(); - } + protected Collection findAllInTable(String tableName) { + return jdbc.query("SELECT * FROM " + tableName + " ORDER BY id", rowMapper) + .stream() + .toList(); + } - protected boolean checkIdIsNotPresentInTable(long id, String tableName) { - String sql = "SELECT ID FROM " + tableName + " WHERE ID = ?"; - return jdbc.queryForList(sql, Long.class, id).isEmpty(); - } + protected boolean checkIdIsNotPresentInTable(long id, String tableName) { + String sql = "SELECT ID FROM " + tableName + " WHERE ID = ?"; + return jdbc.queryForList(sql, Long.class, id).isEmpty(); + } - protected long insertWithKeyHolder(String query, Object... params) { - GeneratedKeyHolder keyHolder = new GeneratedKeyHolder(); - jdbc.update(connection -> { - PreparedStatement ps = connection - .prepareStatement(query, Statement.RETURN_GENERATED_KEYS); - for (int idx = 0; idx < params.length; idx++) { - ps.setObject(idx + 1, params[idx]); - } - return ps; - }, keyHolder); + protected long insertWithKeyHolder(String query, Object... params) { + GeneratedKeyHolder keyHolder = new GeneratedKeyHolder(); + jdbc.update(connection -> { + PreparedStatement ps = connection + .prepareStatement(query, Statement.RETURN_GENERATED_KEYS); + for (int idx = 0; idx < params.length; idx++) { + ps.setObject(idx + 1, params[idx]); + } + return ps; + }, keyHolder); - Long id = keyHolder.getKeyAs(Long.class); - if (id != null) { - return id; - } else { - throw new InternalServerException("Не удалось сохранить данные"); - } - } + Long id; + try { + id = keyHolder.getKeyAs(Long.class); + } catch (DataRetrievalFailureException e) { + Integer id1 = keyHolder.getKeyAs(Integer.class); + if (id1 == null) { + throw new InternalServerException("Не удалось получить id элемента"); + } + id = (long) id1; + } + if (id != null) { + return id; + } else { + throw new InternalServerException("Не удалось сохранить данные"); + } + } - protected void updateWithControl(String query, Object... params) { - int rowsUpdated = jdbc.update(query, params); - if (rowsUpdated == 0) { - throw new InternalServerException("Не удалось обновить данные"); - } - } + protected void updateWithControl(String query, Object... params) { + int rowsUpdated = jdbc.update(query, params); + if (rowsUpdated == 0) { + throw new InternalServerException("Не удалось обновить данные"); + } + } + + protected List findManyByQuery(String query, Object... params) { + return jdbc.query(query, rowMapper, params); + } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/database/DirectorDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/dal/database/DirectorDbStorage.java new file mode 100644 index 0000000..440d58c --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/database/DirectorDbStorage.java @@ -0,0 +1,66 @@ +package ru.yandex.practicum.filmorate.dal.database; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.dal.DirectorStorage; +import ru.yandex.practicum.filmorate.model.Director; + +import java.util.Collection; +import java.util.Optional; + +@Repository +@Qualifier("directorDbStorage") +public class DirectorDbStorage extends BaseDbStorage implements DirectorStorage { + + public DirectorDbStorage( + JdbcTemplate jdbc, + @Qualifier("directorRowMapper") RowMapper rowMapper + ) { + super(jdbc, rowMapper); + } + + @Override + public Optional findById(long id) { + return findOneByIdInTable(id, "directors"); + } + + @Override + public Collection findAll() { + return findAllInTable("directors"); + } + + @Override + public Director createDirector(Director director) { + int id = (int) insertWithKeyHolder( + "INSERT INTO directors (director_name) VALUES (?)", + director.getName() + ); + director.setId(id); + return director; + } + + @Override + public Director updateDirector(Director director) { + updateWithControl( + "UPDATE directors SET director_name= ? WHERE id = ?", + director.getName(), + director.getId() + ); + return director; + } + + @Override + public void deleteDirector(Integer directorId) { + updateWithControl( + "DELETE FROM directors WHERE id = ?", + directorId + ); + } + + public boolean checkDirectorIsNotPresent(int directorId) { + return checkIdIsNotPresentInTable(directorId, "directors"); + } + +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/database/EventDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/dal/database/EventDbStorage.java new file mode 100644 index 0000000..c6a01a8 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/database/EventDbStorage.java @@ -0,0 +1,49 @@ +package ru.yandex.practicum.filmorate.dal.database; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.dal.EventStorage; +import ru.yandex.practicum.filmorate.dal.rowmappers.EventRowMapper; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.model.Event; + +import java.util.Collection; + +@Repository +@Qualifier("eventDbStorage") +public class EventDbStorage extends BaseDbStorage implements EventStorage { + private static final String GET_USER_FEEDS = + "SELECT * FROM events " + + "WHERE user_id = ? " + + "ORDER BY created_at;"; + + private static final String ADD_USER_EVENT = + "INSERT INTO events (user_id, created_at, event_type, operation, entity_id) VALUES (?, ?, ?, ?, ?);"; + + @Autowired + public EventDbStorage(JdbcTemplate jdbc, EventRowMapper rowMapper) { + super(jdbc, rowMapper); + } + + @Override + public Collection getAllEventsByUserId(long userId) { + return findManyByQuery(GET_USER_FEEDS, userId); + } + + public Event addEvent(Event event) { + if (event == null) { + throw new NotFoundException("Событие для добавления не найдено"); + } + + long eventId = insertWithKeyHolder(ADD_USER_EVENT, + event.getUserId(), + event.getTimestamp(), + event.getEventType().name(), + event.getOperation().name(), + event.getEntityId()); + event.setEventId(eventId); + return event; + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/database/FilmDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/dal/database/FilmDbStorage.java index cc53252..eac5594 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dal/database/FilmDbStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/database/FilmDbStorage.java @@ -21,10 +21,7 @@ public class FilmDbStorage extends BaseDbStorage implements FilmStorage { @Autowired - public FilmDbStorage( - JdbcTemplate jdbc, - @Qualifier("filmRowMapper") RowMapper mapper - ) { + public FilmDbStorage(JdbcTemplate jdbc, @Qualifier("filmRowMapper") RowMapper mapper) { super(jdbc, mapper); } @@ -39,13 +36,14 @@ public Film createFilm(Film film) { film.getDuration().toMinutes(), film.getMpaId() ); - film.setId(id); insertGenreIds(film); - return film; + insertDirectorIds(film); + return findById(id).get(); } @Override + @Transactional public Film updateFilm(Film film) { updateWithControl( SQL_FILMS_UPDATE, @@ -56,7 +54,9 @@ public Film updateFilm(Film film) { film.getMpaId(), film.getId() ); - return film; + insertGenreIds(film); + insertDirectorIds(film); + return findById(film.getId()).get(); } @Override @@ -64,7 +64,7 @@ public Film updateFilm(Film film) { public Optional findById(long filmId) { Optional filmOptional = findOneByIdInTable(filmId, "films"); filmOptional.ifPresent(film -> film.setGenreIds(getGenreIdsByFilmId(filmId))); - + filmOptional.ifPresent(film -> film.setDirectorIds(getDirectorIdsByFilmId(filmId))); return filmOptional; } @@ -83,28 +83,97 @@ public void removeLike(long filmId, long userId) { updateWithControl(SQL_FILMS_DELETE_LIKE, filmId, userId); } + @Override + public Collection getALLFilmsOfDirector(Integer directorId) { + return findManyFilms(SQL_FILMS_FIND_ALL_OF_DIRECTOR, directorId); + } + @Override public Collection getTop(int top) { return findManyFilms(SQL_FILMS_FIND_TOP, top); } + @Override + public Collection getTopByFilters(Integer top, Integer genreId, String year) { + Collection films; + if (genreId != null && year != null && !year.isEmpty()) { + films = findManyByQuery(SQL_FILMS_FIND_TOP_BY_GENRE_AND_YEAR, Integer.parseInt(year), genreId, top); + } else if (year != null && !year.isEmpty()) { + films = findManyByQuery(SQL_FILMS_FIND_TOP_BY_YEAR, Integer.parseInt(year), top); + } else if (genreId != null) { + films = findManyByQuery(SQL_FILMS_FIND_TOP_BY_GENRES, genreId, top); + } else { + films = findManyByQuery(SQL_FILMS_FIND_TOP, top); + } + return films.stream().peek(film -> { + Set genres = getGenreIdsByFilmId(film.getId()); + film.setGenreIds(new HashSet<>(genres)); + Set directors = getDirectorIdsByFilmId(film.getId()); + film.setDirectorIds(new HashSet<>(directors)); + }).toList(); + } + @Override public boolean checkFilmIsNotPresent(Long filmId) { return checkIdIsNotPresentInTable(filmId, "films"); } + @Override + public Map> getAllLikes() { + String sql = "SELECT user_id, film_id FROM likes"; + Map> allLikes = new HashMap<>(); + jdbc.query(sql, (rs) -> { + long userId = rs.getLong("user_id"); + long filmId = rs.getLong("film_id"); + allLikes.computeIfAbsent(userId, k -> new HashSet<>()).add(filmId); + }); + return allLikes; + } + + @Override + public Collection getCommonLikedFilms(long userId, long friendId) { + return findManyFilms(SQL_FILMS_FIND_COMMON_LIKED, userId, friendId); + } + + @Override + public Collection search(String query, String by) { + String searchPattern = "%" + query.toLowerCase() + "%"; + String condition; + if (by.contains("director") && by.contains("title")) { + condition = "(LOWER(f.film_name) LIKE ? OR LOWER(d.director_name) LIKE ?)"; + return findManyFilms(String.format(SQL_FILMS_SEARCH, condition), searchPattern, searchPattern); + } else if (by.contains("director")) { + condition = "LOWER(d.director_name) LIKE ?"; + return findManyFilms(String.format(SQL_FILMS_SEARCH, condition), searchPattern); + } else { + condition = "LOWER(f.film_name) LIKE ?"; + return findManyFilms(String.format(SQL_FILMS_SEARCH, condition), searchPattern); + } + } + + @Override + public void removeFilm(long filmId) { + updateWithControl("DELETE FROM films WHERE id = ?", filmId); + } + private void insertGenreIds(Film film) { + jdbc.update("DELETE FROM genres_of_films WHERE film_id = ?", film.getId()); Set genreIds = film.getGenreIds(); - if (genreIds.isEmpty()) { - return; - } - String placeholders = String.join(",", - Collections.nCopies(genreIds.size(), " (" + film.getId() + ", ?)") - ); - String sql = "INSERT INTO GENRES_OF_FILMS (FILM_ID, GENRE_ID) VALUES" + placeholders; + if (genreIds == null || genreIds.isEmpty()) return; + String placeholders = String.join(",", Collections.nCopies(genreIds.size(), " (" + film.getId() + ", ?)")); + String sql = SQL_FILMS_INSERT_GENREIDS + placeholders; updateWithControl(sql, genreIds.toArray()); } + private void insertDirectorIds(Film film) { + jdbc.update("DELETE FROM directors_of_films WHERE film_id = ?", film.getId()); + Set directorIds = film.getDirectorIds(); + if (directorIds == null || directorIds.isEmpty()) return; + String placeholders = String.join(",", Collections.nCopies(directorIds.size(), " (" + film.getId() + ", ?)")); + String sql = SQL_FILMS_INSERT_DIRECTORIDS + placeholders; + updateWithControl(sql, directorIds.toArray()); + } + private Collection findManyFilms(String query, Object... params) { return jdbc.query(query, rs -> { Map films = new LinkedHashMap<>(); @@ -112,27 +181,19 @@ private Collection findManyFilms(String query, Object... params) { while (rs.next()) { long filmId = rs.getLong("id"); Film film = films.computeIfAbsent(filmId, id -> { - try { return rowMapper.mapRow(rs, rowNum.getAndIncrement()); } catch (SQLException e) { - throw new InternalServerException( - "Не удалось получить все фильмы из базы.\n" + e.getMessage() - ); + throw new InternalServerException("Не удалось получить все фильмы из базы.\n" + e.getMessage()); } }); - long userId = rs.getLong("user_id"); - if (!rs.wasNull() && film != null) { - film.addLike(userId); - } - + if (!rs.wasNull() && film != null) film.addLike(userId); int genreId = rs.getInt("genre_id"); - if (!rs.wasNull() && film != null) { - film.addGenreId(genreId); - } + if (!rs.wasNull() && film != null) film.addGenreId(genreId); + int directorId = rs.getInt("director_id"); + if (!rs.wasNull() && film != null) film.addDirectorId(directorId); } - return new ArrayList<>(films.values()); }, params); } @@ -141,4 +202,28 @@ private Set getGenreIdsByFilmId(long filmId) { return findColumnByQuery(SQL_FILMS_FIND_GENREIDS_BY_FILM_ID, Integer.class, filmId); } -} + private Set getDirectorIdsByFilmId(long filmId) { + return findColumnByQuery(SQL_FILMS_FIND_DIRECTORIDS_BY_FILM_ID, Integer.class, filmId); + } + + @Override + public Collection getFilmsByIds(Collection ids) { + if (ids == null || ids.isEmpty()) { + return Collections.emptyList(); + } + + String inSql = String.join(",", Collections.nCopies(ids.size(), "?")); + + String sql = String.format(""" + SELECT f.id, f.film_name, f.description, f.release_date, f.duration, f.mpa_id, + l.user_id, gof.genre_id, dof.director_id + FROM films f + LEFT JOIN likes l ON f.id = l.film_id + LEFT JOIN genres_of_films gof ON f.id = gof.film_id + LEFT JOIN directors_of_films dof ON f.id = dof.film_id + WHERE f.id IN (%s) + """, inSql); + + return findManyFilms(sql, ids.toArray()); + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/database/GenreDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/dal/database/GenreDbStorage.java index 0bda6a2..53838b8 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dal/database/GenreDbStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/database/GenreDbStorage.java @@ -8,7 +8,10 @@ import ru.yandex.practicum.filmorate.dal.GenreStorage; import ru.yandex.practicum.filmorate.model.Genre; -import java.util.*; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; @Repository @Qualifier("genreDbStorage") @@ -32,9 +35,9 @@ public Collection getAllGenres() { @Override public Map getMapOfAllGenres() { Map allGenres = new HashMap<>(); - findAllInTable("genres") + findAllInTable("genres") .forEach(genre -> allGenres.put(genre.getId(), genre)); - return allGenres; + return allGenres; } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/database/ReviewDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/dal/database/ReviewDbStorage.java new file mode 100644 index 0000000..cd09399 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/database/ReviewDbStorage.java @@ -0,0 +1,133 @@ +package ru.yandex.practicum.filmorate.dal.database; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; +import ru.yandex.practicum.filmorate.dal.ReviewStorage; +import ru.yandex.practicum.filmorate.model.Review; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import static ru.yandex.practicum.filmorate.dal.database.sql.ReviewQueries.*; + +@Repository +@Qualifier("reviewDbStorage") +public class ReviewDbStorage extends BaseDbStorage implements ReviewStorage { + + @Autowired + public ReviewDbStorage(JdbcTemplate jdbc, @Qualifier("reviewRowMapper") RowMapper mapper) { + super(jdbc, mapper); + } + + @Transactional + @Override + public Review createReview(Review review) { + long id = insertWithKeyHolder( + SQL_REVIEWS_INSERT, + review.getContent(), + review.getIsPositive(), + review.getUserId(), + review.getFilmId() + ); + review.setId(id); + return review; + } + + @Override + public Review updateReview(Review review) { + updateWithControl( + SQL_REVIEWS_UPDATE, + review.getContent(), + review.getIsPositive(), + review.getId() + ); + return findReviewById(review.getId()).get(); + } + + @Override + public void deleteReview(long id) { + updateWithControl(SQL_REVIEWS_DELETE, id); + } + + @Override + public Optional findReviewById(long reviewId) { + Optional review = findOneByIdInTable(reviewId, "reviews"); + if (review.isPresent()) { + review.get().setUserLikes(findAllReactionsByReviewId(reviewId)); + return review; + } + return Optional.empty(); + } + + @Override + public Collection findReviewsByFilmId(Long filmId, int count) { + Collection reviews; + if (filmId == null) { + reviews = findManyByQuery(SQL_REVIEWS_FIND_ALL, count); + } else { + reviews = findManyByQuery(SQL_REVIEWS_FIND_BY_FILM_ID, filmId, count); + } + reviews.forEach(review -> { + review.setUserLikes(findAllReactionsByReviewId(review.getId())); + }); + return reviews; + } + + @Transactional + @Override + public void addReviewLike(long reviewId, long userId) { + updateWithControl(SQL_REVIEWS_ADD_LIKE, reviewId, userId); + updateWithControl(SQL_REVIEWS_CALCULATE_USEFUL, reviewId, reviewId); + } + + @Transactional + @Override + public void addReviewDislike(long reviewId, long userId) { + updateWithControl(SQL_REVIEWS_ADD_DISLIKE, reviewId, userId); + updateWithControl(SQL_REVIEWS_CALCULATE_USEFUL, reviewId, reviewId); + } + + @Transactional + @Override + public void removeReviewLike(long reviewId, long userId) { + updateWithControl(SQL_REVIEWS_DELETE_LIKE, reviewId, userId); + updateWithControl(SQL_REVIEWS_CALCULATE_USEFUL, reviewId, reviewId); + } + + @Transactional + @Override + public void removeReviewDislike(long reviewId, long userId) { + updateWithControl(SQL_REVIEWS_DELETE_DISLIKE, reviewId, userId); + updateWithControl(SQL_REVIEWS_CALCULATE_USEFUL, reviewId, reviewId); + } + + @Override + public boolean checkReviewIsNotPresent(long reviewId) { + return checkIdIsNotPresentInTable(reviewId, "reviews"); + } + + public Optional getReactionsStatus(long reviewId, long userId) { + return jdbc.query(SQL_REVIEWS_LIKE_STATUS, (rs, rowNum) -> rs.getBoolean("is_like"), reviewId, userId) + .stream() + .findFirst(); + } + + private Map findAllReactionsByReviewId(long reviewId) { + return jdbc.query(SQL_REVIEWS_GET_LIKES_BY_REVIEW_ID, rs -> { + Map reactions = new HashMap<>(); + while (rs.next()) { + reactions.put( + rs.getLong("user_id"), + rs.getBoolean("is_like") + ); + } + return reactions; + }, reviewId); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/database/UserDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/dal/database/UserDbStorage.java index be9feb6..15a3bad 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dal/database/UserDbStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/database/UserDbStorage.java @@ -97,6 +97,11 @@ public Set getFriendIds(long userId) { return findColumnByQuery(sql, Long.class, userId); } + @Override + public void removeUser(long userId) { + updateWithControl("DELETE FROM users WHERE id = ?", userId); + } + @Override public boolean checkUserIsNotPresent(Long userId) { return checkIdIsNotPresentInTable(userId, "users"); diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/database/sql/FilmQueryes.java b/src/main/java/ru/yandex/practicum/filmorate/dal/database/sql/FilmQueryes.java index dc43660..6b21bf1 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dal/database/sql/FilmQueryes.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/database/sql/FilmQueryes.java @@ -10,6 +10,7 @@ public class FilmQueryes { mrg.DURATION, mrg.MPA_ID, gof.GENRE_ID, + dof.DIRECTOR_ID, user_id FROM ( SELECT f.ID, @@ -23,7 +24,43 @@ public class FilmQueryes { LEFT JOIN LIKES l ON f.ID = l.FILM_ID ORDER BY f.ID ) AS mrg - LEFT JOIN GENRES_OF_FILMS gof ON mrg.id = gof.FILM_ID; + LEFT JOIN GENRES_OF_FILMS gof ON mrg.id = gof.FILM_ID + LEFT JOIN DIRECTORS_OF_FILMS dof ON mrg.ID = dof.FILM_ID + """; + + public static final String SQL_FILMS_FIND_ALL_OF_DIRECTOR = """ + SELECT mrg.id, + mrg.FILM_NAME, + mrg.DESCRIPTION, + mrg.RELEASE_DATE, + mrg.DURATION, + mrg.MPA_ID, + gof.GENRE_ID, + dof.DIRECTOR_ID, + user_id + FROM ( + SELECT f.ID, + f.FILM_NAME, + DESCRIPTION, + RELEASE_DATE, + DURATION, + MPA_ID, + l.USER_ID + FROM FILMS f + LEFT JOIN LIKES l ON f.ID = l.FILM_ID + ORDER BY f.ID + ) AS mrg + LEFT JOIN GENRES_OF_FILMS gof ON mrg.id = gof.FILM_ID + LEFT JOIN DIRECTORS_OF_FILMS dof ON mrg.ID = dof.FILM_ID + WHERE mrg.ID IN ( + SELECT f1.ID + FROM FILMS f1 + WHERE f1.ID IN ( + SELECT dof2.FILM_ID + FROM DIRECTORS_OF_FILMS dof2 + WHERE dof2.DIRECTOR_ID = ? + ) + ) """; public static final String SQL_FILMS_FIND_TOP = """ @@ -34,6 +71,7 @@ public class FilmQueryes { mrg.DURATION, mrg.MPA_ID, gof.GENRE_ID, + dof.DIRECTOR_ID, user_id FROM ( SELECT f.ID, @@ -47,6 +85,7 @@ public class FilmQueryes { LEFT JOIN LIKES l ON f.ID = l.FILM_ID ) AS mrg LEFT JOIN GENRES_OF_FILMS gof ON mrg.id = gof.FILM_ID + LEFT JOIN DIRECTORS_OF_FILMS dof ON mrg.ID = dof.FILM_ID WHERE mrg.id IN ( SELECT rte.ID FROM ( @@ -58,12 +97,12 @@ WHERE mrg.id IN ( ) AS rte ORDER BY rate DESC, rte.ID LIMIT ? - ); + ) """; public static final String SQL_FILMS_SET_LIKE = """ INSERT INTO LIKES (FILM_ID, USER_ID) - VALUES (?, ?); + VALUES (?, ?) """; public static final String SQL_FILMS_DELETE_LIKE = """ @@ -79,6 +118,21 @@ INSERT INTO LIKES (FILM_ID, USER_ID) WHERE gof.FILM_ID = ? """; + public static final String SQL_FILMS_INSERT_GENREIDS = """ + MERGE INTO GENRES_OF_FILMS (FILM_ID, GENRE_ID) KEY (FILM_ID, GENRE_ID) VALUES + """; + + public static final String SQL_FILMS_FIND_DIRECTORIDS_BY_FILM_ID = """ + SELECT d.ID + FROM DIRECTORS d + JOIN DIRECTORS_OF_FILMS dof ON d.ID = dof.DIRECTOR_ID + WHERE dof.FILM_ID = ? + """; + + public static final String SQL_FILMS_INSERT_DIRECTORIDS = """ + MERGE INTO DIRECTORS_OF_FILMS (FILM_ID, DIRECTOR_ID) KEY (FILM_ID, DIRECTOR_ID) VALUES + """; + public static final String SQL_FILMS_INSERT = """ INSERT INTO films ( film_name, @@ -99,4 +153,80 @@ INSERT INTO films ( mpa_id = ? WHERE id = ? """; + + public static final String SQL_FILMS_FIND_TOP_BY_GENRES = """ + SELECT f.*, COUNT(fl.user_id) as likes_count + FROM films f + LEFT JOIN likes fl ON f.id = fl.film_id + LEFT JOIN genres_of_films gr ON f.id = gr.film_id + WHERE gr.genre_id = ? + GROUP BY f.id + ORDER BY likes_count DESC + LIMIT ? + """; + + public static final String SQL_FILMS_FIND_TOP_BY_YEAR = """ + SELECT f.*, COUNT(fl.user_id) as likes_count + FROM (SELECT * FROM films WHERE EXTRACT(YEAR FROM release_date) = ?) f + LEFT JOIN likes fl ON f.id = fl.film_id + GROUP BY f.id + ORDER BY likes_count DESC + LIMIT ? + """; + + public static final String SQL_FILMS_FIND_TOP_BY_GENRE_AND_YEAR = """ + SELECT f.*, COUNT(fl.user_id) as likes_count + FROM (SELECT * FROM films WHERE EXTRACT(YEAR FROM release_date) = ?) f + LEFT JOIN likes fl ON f.id = fl.film_id + LEFT JOIN genres_of_films gr ON f.id = gr.film_id + WHERE gr.genre_id = ? + GROUP BY f.id + ORDER BY likes_count DESC + LIMIT ? + """; + + public static final String SQL_FILMS_FIND_COMMON_LIKED = """ + SELECT f.id, + f.film_name, + f.description, + f.release_date, + f.duration, + f.mpa_id, + l_all.user_id, + g.genre_id, + d.director_id, + (SELECT COUNT(*) FROM likes WHERE film_id = f.id) as likes_count + FROM films f + LEFT JOIN likes l_all ON f.id = l_all.film_id + LEFT JOIN genres_of_films g ON f.id = g.film_id + LEFT JOIN directors_of_films d ON f.id = d.film_id + WHERE f.id IN ( + SELECT l1.film_id + FROM likes l1 + JOIN likes l2 ON l1.film_id = l2.film_id + WHERE l1.user_id = ? AND l2.user_id = ? + ) + ORDER BY likes_count DESC; + """; + + public static final String SQL_FILMS_SEARCH = """ + SELECT f.id, + f.film_name, + f.description, + f.release_date, + f.duration, + f.mpa_id, + m.mpa_name, + gof.genre_id, + dof.director_id, + l.user_id + FROM films f + LEFT JOIN mpa m ON f.mpa_id = m.id + LEFT JOIN genres_of_films gof ON f.id = gof.film_id + LEFT JOIN directors_of_films dof ON f.id = dof.film_id + LEFT JOIN directors d ON dof.director_id = d.id + LEFT JOIN likes l ON f.id = l.film_id + WHERE %s + ORDER BY (SELECT COUNT(*) FROM likes WHERE film_id = f.id) DESC, f.id ASC; + """; } diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/database/sql/ReviewQueries.java b/src/main/java/ru/yandex/practicum/filmorate/dal/database/sql/ReviewQueries.java new file mode 100644 index 0000000..96b9ed5 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/database/sql/ReviewQueries.java @@ -0,0 +1,45 @@ +package ru.yandex.practicum.filmorate.dal.database.sql; + +public class ReviewQueries { + + public static final String SQL_REVIEWS_INSERT = + "INSERT INTO reviews (content, is_positive, user_id, film_id) VALUES (?, ?, ?, ?)"; + + public static final String SQL_REVIEWS_UPDATE = + "UPDATE reviews SET content = ?, is_positive = ? WHERE id = ?"; + + public static final String SQL_REVIEWS_DELETE = + "DELETE FROM reviews WHERE id = ?"; + + public static final String SQL_REVIEWS_FIND_ALL = + "SELECT * FROM reviews ORDER BY useful DESC LIMIT ?"; + + public static final String SQL_REVIEWS_FIND_BY_FILM_ID = + "SELECT * FROM reviews WHERE film_id = ? ORDER BY useful DESC LIMIT ?"; + + public static final String SQL_REVIEWS_ADD_LIKE = + "INSERT INTO review_likes (review_id, user_id, is_like) VALUES (?, ?, TRUE)"; + + public static final String SQL_REVIEWS_ADD_DISLIKE = + "INSERT INTO review_likes (review_id, user_id, is_like) VALUES (?, ?, FALSE)"; + + public static final String SQL_REVIEWS_DELETE_LIKE = + "DELETE FROM review_likes WHERE review_id = ? AND user_id = ? AND is_like = TRUE"; + + public static final String SQL_REVIEWS_DELETE_DISLIKE = + "DELETE FROM review_likes WHERE review_id = ? AND user_id = ? AND is_like = FALSE"; + + public static final String SQL_REVIEWS_LIKE_STATUS = + "SELECT is_like FROM review_likes WHERE review_id = ? AND user_id = ?"; + + public static final String SQL_REVIEWS_GET_LIKES_BY_REVIEW_ID = + "SELECT user_id, is_like FROM review_likes WHERE review_id = ?"; + + public static final String SQL_REVIEWS_CALCULATE_USEFUL = """ + UPDATE reviews SET useful = ( + SELECT COALESCE(SUM(CASE WHEN is_like = TRUE THEN 1 ELSE -1 END), 0) + FROM review_likes + WHERE review_id = ? + ) WHERE id = ? + """; +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/inmemory/InMemoryFilmStorage.java b/src/main/java/ru/yandex/practicum/filmorate/dal/inmemory/InMemoryFilmStorage.java index 80a1493..bdbb0ed 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dal/inmemory/InMemoryFilmStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/inmemory/InMemoryFilmStorage.java @@ -1,52 +1,41 @@ package ru.yandex.practicum.filmorate.dal.inmemory; -import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Component; import ru.yandex.practicum.filmorate.dal.FilmStorage; import ru.yandex.practicum.filmorate.exception.MethodNotImplementedException; import ru.yandex.practicum.filmorate.model.Film; -import ru.yandex.practicum.filmorate.servise.util.IdentifyService; import java.util.Collection; import java.util.Map; import java.util.Optional; +import java.util.Set; @Component -@Qualifier("inMemoryFilmStorage") -@RequiredArgsConstructor public class InMemoryFilmStorage implements FilmStorage { - private final Map films; - private final IdentifyService identifyService; - @Override public Optional findById(long id) { - return films.values().stream() - .filter(f -> f.getId().equals(id)).findFirst(); + throw new MethodNotImplementedException(); } @Override public Collection findAll() { - return films.values(); + throw new MethodNotImplementedException(); } @Override public Film createFilm(Film film) { - film.setId(identifyService.getNextId(films)); - films.put(film.getId(), film); - return film; + throw new MethodNotImplementedException(); } @Override public Film updateFilm(Film film) { - films.put(film.getId(), film); - return film; + throw new MethodNotImplementedException(); } @Override public boolean checkFilmIsNotPresent(Long filmId) { - return films.containsKey(filmId); + throw new MethodNotImplementedException(); } @Override @@ -55,7 +44,7 @@ public Collection getTop(int count) { } @Override - public void removeLike(long filmId, long userId) { + public Collection getTopByFilters(Integer count, Integer genreId, String year) { throw new MethodNotImplementedException(); } @@ -63,4 +52,39 @@ public void removeLike(long filmId, long userId) { public void setLike(long filmId, long userId) { throw new MethodNotImplementedException(); } -} + + @Override + public void removeLike(long filmId, long userId) { + throw new MethodNotImplementedException(); + } + + @Override + public Collection getALLFilmsOfDirector(Integer directorId) { + throw new MethodNotImplementedException(); + } + + @Override + public Map> getAllLikes() { + throw new MethodNotImplementedException(); + } + + @Override + public Collection getCommonLikedFilms(long userId, long friendId) { + throw new MethodNotImplementedException(); + } + + @Override + public Collection search(String query, String by) { + throw new MethodNotImplementedException(); + } + + @Override + public void removeFilm(long filmId) { + throw new MethodNotImplementedException(); + } + + @Override + public Collection getFilmsByIds(Collection ids) { + throw new MethodNotImplementedException(); + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/inmemory/InMemoryUserStorage.java b/src/main/java/ru/yandex/practicum/filmorate/dal/inmemory/InMemoryUserStorage.java index 3c2df8e..620be32 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dal/inmemory/InMemoryUserStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/inmemory/InMemoryUserStorage.java @@ -6,7 +6,7 @@ import ru.yandex.practicum.filmorate.dal.UserStorage; import ru.yandex.practicum.filmorate.exception.MethodNotImplementedException; import ru.yandex.practicum.filmorate.model.User; -import ru.yandex.practicum.filmorate.servise.util.IdentifyService; +import ru.yandex.practicum.filmorate.service.util.IdentifyService; import java.util.Collection; import java.util.Map; @@ -73,4 +73,9 @@ public Collection getFriendIds(long userId) { throw new MethodNotImplementedException(); } + @Override + public void removeUser(long userId) { + throw new MethodNotImplementedException(); + } + } diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/rowmappers/DirectorRowMapper.java b/src/main/java/ru/yandex/practicum/filmorate/dal/rowmappers/DirectorRowMapper.java new file mode 100644 index 0000000..e69af46 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/rowmappers/DirectorRowMapper.java @@ -0,0 +1,22 @@ +package ru.yandex.practicum.filmorate.dal.rowmappers; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Component; +import ru.yandex.practicum.filmorate.model.Director; + +import java.sql.ResultSet; +import java.sql.SQLException; + +@Component +@Qualifier("directorRowMapper") +public class DirectorRowMapper implements RowMapper { + + @Override + public Director mapRow(ResultSet resultSet, int rowNum) throws SQLException { + return Director.builder() + .id(resultSet.getInt("id")) + .name(resultSet.getString("director_name")) + .build(); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/rowmappers/EventRowMapper.java b/src/main/java/ru/yandex/practicum/filmorate/dal/rowmappers/EventRowMapper.java new file mode 100644 index 0000000..679de25 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/rowmappers/EventRowMapper.java @@ -0,0 +1,27 @@ +package ru.yandex.practicum.filmorate.dal.rowmappers; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Component; +import ru.yandex.practicum.filmorate.model.Event; +import ru.yandex.practicum.filmorate.model.EventType; +import ru.yandex.practicum.filmorate.model.Operation; + +import java.sql.ResultSet; +import java.sql.SQLException; + +@Component +@Qualifier("eventRowMapper") +public class EventRowMapper implements RowMapper { + @Override + public Event mapRow(ResultSet resultSet, int rowNum) throws SQLException { + return Event.builder() + .eventId(resultSet.getLong("id")) + .userId(resultSet.getLong("user_id")) + .timestamp(resultSet.getTimestamp("created_at").toInstant()) + .eventType(EventType.valueOf(resultSet.getString("event_type"))) + .operation(Operation.valueOf(resultSet.getString("operation"))) + .entityId(resultSet.getLong("entity_id")) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/rowmappers/ReviewRowMapper.java b/src/main/java/ru/yandex/practicum/filmorate/dal/rowmappers/ReviewRowMapper.java new file mode 100644 index 0000000..d7bb879 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/rowmappers/ReviewRowMapper.java @@ -0,0 +1,26 @@ +package ru.yandex.practicum.filmorate.dal.rowmappers; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Component; +import ru.yandex.practicum.filmorate.model.Review; + +import java.sql.ResultSet; +import java.sql.SQLException; + +@Component +@Qualifier("reviewRowMapper") +public class ReviewRowMapper implements RowMapper { + + @Override + public Review mapRow(ResultSet resultSet, int rowNum) throws SQLException { + return Review.builder() + .id(resultSet.getLong("id")) + .content(resultSet.getString("content")) + .isPositive(resultSet.getBoolean("is_positive")) + .userId(resultSet.getLong("user_id")) + .filmId(resultSet.getLong("film_id")) + .useful(resultSet.getLong("useful")) + .build(); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/rowmappers/UserRowMapper.java b/src/main/java/ru/yandex/practicum/filmorate/dal/rowmappers/UserRowMapper.java index 3734215..a6e6748 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dal/rowmappers/UserRowMapper.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/rowmappers/UserRowMapper.java @@ -12,15 +12,15 @@ @Component @Qualifier("userRowMapper") public class UserRowMapper implements RowMapper { - @Override - public User mapRow(ResultSet resultSet, int rowNum) throws SQLException { - return User.builder() - .id(resultSet.getLong("id")) - .name(resultSet.getString("user_name")) - .login(resultSet.getString("login")) - .email(resultSet.getString("email")) - .birthday(resultSet.getDate("birthday").toLocalDate()) - .friendsIds(new HashSet<>()) - .build(); - } + @Override + public User mapRow(ResultSet resultSet, int rowNum) throws SQLException { + return User.builder() + .id(resultSet.getLong("id")) + .name(resultSet.getString("user_name")) + .login(resultSet.getString("login")) + .email(resultSet.getString("email")) + .birthday(resultSet.getDate("birthday").toLocalDate()) + .friendsIds(new HashSet<>()) + .build(); + } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/DirectorDto.java b/src/main/java/ru/yandex/practicum/filmorate/dto/DirectorDto.java new file mode 100644 index 0000000..97c3e6f --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/DirectorDto.java @@ -0,0 +1,20 @@ +package ru.yandex.practicum.filmorate.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.FieldDefaults; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE) +public class DirectorDto { + + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + int id; + + String name; +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/EventDto.java b/src/main/java/ru/yandex/practicum/filmorate/dto/EventDto.java new file mode 100644 index 0000000..6f66634 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/EventDto.java @@ -0,0 +1,19 @@ +package ru.yandex.practicum.filmorate.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Data; +import ru.yandex.practicum.filmorate.model.EventType; +import ru.yandex.practicum.filmorate.model.Operation; + +@Data +@Builder +public class EventDto { + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + private Long eventId; + private Long userId; + private Long timestamp; + private EventType eventType; + private Operation operation; + private Long entityId; +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/FilmDto.java b/src/main/java/ru/yandex/practicum/filmorate/dto/FilmDto.java index cf82f7b..ba35e8f 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dto/FilmDto.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/FilmDto.java @@ -10,9 +10,9 @@ import java.util.ArrayList; import java.util.Collection; +@JsonPropertyOrder @Data @Builder -@JsonPropertyOrder public class FilmDto { @JsonProperty(access = JsonProperty.Access.READ_ONLY) private long id; @@ -29,4 +29,7 @@ public class FilmDto { @Builder.Default private Collection genres = new ArrayList<>(); + + @Builder.Default + private Collection directors = new ArrayList<>(); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/MpaDto.java b/src/main/java/ru/yandex/practicum/filmorate/dto/MpaDto.java index b8475d7..ce1d998 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dto/MpaDto.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/MpaDto.java @@ -8,5 +8,4 @@ public class MpaDto extends Mpa { public MpaDto(int id, String name) { super(id, name); } - } diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/ReviewDto.java b/src/main/java/ru/yandex/practicum/filmorate/dto/ReviewDto.java new file mode 100644 index 0000000..c9013d4 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/ReviewDto.java @@ -0,0 +1,15 @@ +package ru.yandex.practicum.filmorate.dto; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class ReviewDto { + private Long reviewId; + private String content; + private Boolean isPositive; + private Long userId; + private Long filmId; + private Long useful; +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/UserDto.java b/src/main/java/ru/yandex/practicum/filmorate/dto/UserDto.java index cb67197..8bbc7e9 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dto/UserDto.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/UserDto.java @@ -11,13 +11,13 @@ @Data @Builder public class UserDto { - @JsonProperty(access = JsonProperty.Access.READ_ONLY) - private long id; - private String name; - private String login; - private String email; - private LocalDate birthday; + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + private long id; + private String name; + private String login; + private String email; + private LocalDate birthday; - @Builder.Default - private Set friends = new HashSet<>(); + @Builder.Default + private Set friends = new HashSet<>(); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/adapters/MinDate.java b/src/main/java/ru/yandex/practicum/filmorate/dto/adapters/MinDate.java index e62f6f5..16b4fba 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dto/adapters/MinDate.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/adapters/MinDate.java @@ -2,31 +2,35 @@ import jakarta.validation.Constraint; import jakarta.validation.Payload; + import java.lang.annotation.*; /** * Аннотация MinDate используется для проверки того, что дата не может быть раньше указанной минимальной даты. * Она применяется к полям и параметрам и валидируется с помощью MinDateValidator. */ -@Documented @Constraint(validatedBy = MinDateValidator.class) @Target({ElementType.FIELD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) +@Documented public @interface MinDate { /** * Сообщение, которое будет отображаться в случае, если проверка не пройдена. + * * @return сообщение по умолчанию: "Дата не может быть раньше {minDate}" */ String message() default "Дата не может быть раньше {minDate}"; /** * Группы, к которым относится валидация. + * * @return массив классов групп по умолчанию */ Class[] groups() default {}; /** * Пэйлоады, связанные с валидацией. + * * @return массив классов пейлоадов по умолчанию */ Class[] payload() default {}; @@ -34,6 +38,7 @@ /** * Указывает минимальную дату, с которой будет сравниваться проверяемая дата. * Формат даты должен соответствовать шаблону "yyyy-MM-dd". + * * @return минимальная дата для проверки */ String minDate(); diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/adapters/MinDateValidator.java b/src/main/java/ru/yandex/practicum/filmorate/dto/adapters/MinDateValidator.java index 08daa27..63f6cb7 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dto/adapters/MinDateValidator.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/adapters/MinDateValidator.java @@ -3,6 +3,7 @@ import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidatorContext; + import java.time.LocalDate; import java.time.format.DateTimeFormatter; @@ -18,6 +19,7 @@ public class MinDateValidator implements ConstraintValidator /** * Инициализирует валидатор минимальным значением даты, заданным в аннотации. + * * @param constraintAnnotation аннотация @MinDate */ @Override @@ -28,7 +30,8 @@ public void initialize(MinDate constraintAnnotation) { /** * Проверяет, что указанная дата не меньше минимальной даты. * Если проверяемая дата равна null, считается, что проверка прошла успешно. - * @param value проверяемая дата + * + * @param value проверяемая дата * @param context контекст валидации * @return true, если дата не меньше минимальной или равна null, иначе false */ diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/request/create/DirectorCreateRequest.java b/src/main/java/ru/yandex/practicum/filmorate/dto/request/create/DirectorCreateRequest.java new file mode 100644 index 0000000..da66a38 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/request/create/DirectorCreateRequest.java @@ -0,0 +1,16 @@ +package ru.yandex.practicum.filmorate.dto.request.create; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.AccessLevel; +import lombok.Data; +import lombok.experimental.FieldDefaults; + +@Data +@FieldDefaults(level = AccessLevel.PRIVATE) +public class DirectorCreateRequest { + + @Size(min = 2, max = 70, message = "Длина имени режиссера может быть от 2 до 70 символов.") + @NotBlank(message = "Имя режиссера не может быть пустым.") + String name; +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/request/create/FilmCreateRequest.java b/src/main/java/ru/yandex/practicum/filmorate/dto/request/create/FilmCreateRequest.java index e117672..a2ad789 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dto/request/create/FilmCreateRequest.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/request/create/FilmCreateRequest.java @@ -6,7 +6,8 @@ import jakarta.validation.constraints.Size; import lombok.Data; import org.hibernate.validator.constraints.time.DurationMin; -import ru.yandex.practicum.filmorate.dto.adapters.*; +import ru.yandex.practicum.filmorate.dto.adapters.MinDate; +import ru.yandex.practicum.filmorate.model.Director; import ru.yandex.practicum.filmorate.model.Genre; import ru.yandex.practicum.filmorate.model.Mpa; @@ -18,16 +19,16 @@ @Data public class FilmCreateRequest { - @NotNull(message = "Название должно быть указано.") @Size(min = 2, max = 100, message = "Название должно содержать от 2 до 100 символов.") + @NotNull(message = "Название должно быть указано.") private String name; - @NotNull(message = "Описание должно быть указано.") @Size(max = 200, message = "Максимальная длина описания — 200 символов.") + @NotNull(message = "Описание должно быть указано.") private String description; - @NotNull(message = "Дата релиза должна быть указана.") @Past(message = "Дата релиза должна быть в прошлом.") + @NotNull(message = "Дата релиза должна быть указана.") @MinDate(minDate = "1895-12-28") private LocalDate releaseDate; @@ -41,4 +42,6 @@ public class FilmCreateRequest { private Set genres = new HashSet<>(); + private Set directors = new HashSet<>(); + } diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/request/create/ReviewCreateRequest.java b/src/main/java/ru/yandex/practicum/filmorate/dto/request/create/ReviewCreateRequest.java new file mode 100644 index 0000000..23fbd31 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/request/create/ReviewCreateRequest.java @@ -0,0 +1,22 @@ +package ru.yandex.practicum.filmorate.dto.request.create; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Data +public class ReviewCreateRequest { + @NotBlank(message = "Контент отзыва не может быть пустым.") + @Size(max = 2048, message = "Максимальная длина отзыва — 2048 символов.") + private String content; + + @NotNull(message = "Тип отзыва должен быть указан.") + private Boolean isPositive; + + @NotNull(message = "userId должен быть указан.") + private Long userId; + + @NotNull(message = "filmId должен быть указан.") + private Long filmId; +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/request/create/UserCreateRequest.java b/src/main/java/ru/yandex/practicum/filmorate/dto/request/create/UserCreateRequest.java index d8e2a74..b3b39f9 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dto/request/create/UserCreateRequest.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/request/create/UserCreateRequest.java @@ -8,20 +8,19 @@ @Data public class UserCreateRequest { - @Size(min = 2, max = 50, message = "Длина имени пользователя может быть от 2 до 50 символов.") - private String name; + @Size(max = 50, message = "Длина имени пользователя не может быть больше 50 символов.") + private String name; - @NotNull(message = "Логин должен быть указан.") - @Pattern(regexp = "^[A-Za-z0-9_]+$", message = "Логин может состоять только из этих символов [A-Za-z0-9_].") - @Size(min = 3, max = 30, message = "Длина логина может быть от 3 до 20 символов.") + @NotNull(message = "Логин должен быть указан.") + @Pattern(regexp = "^[A-Za-z0-9_]+$", message = "Логин может состоять только из этих символов [A-Za-z0-9_].") + @Size(min = 3, max = 30, message = "Длина логина может быть от 3 до 20 символов.") private String login; - @NotNull(message = "Электронная почта должна быть указана.") - @Email(message = "Не распознан формат адреса электронной почты.") + @NotNull(message = "Электронная почта должна быть указана.") + @Email(message = "Не распознан формат адреса электронной почты.") private String email; - @NotNull(message = "Дата рождения должна быть указана.") - @PastOrPresent(message = "Дата рождения не может быть в будущем.") + @NotNull(message = "Дата рождения должна быть указана.") + @PastOrPresent(message = "Дата рождения не может быть в будущем.") private LocalDate birthday; - } diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/request/update/DirectorUpdateRequest.java b/src/main/java/ru/yandex/practicum/filmorate/dto/request/update/DirectorUpdateRequest.java new file mode 100644 index 0000000..6f930e9 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/request/update/DirectorUpdateRequest.java @@ -0,0 +1,26 @@ +package ru.yandex.practicum.filmorate.dto.request.update; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.Size; +import lombok.AccessLevel; +import lombok.Data; +import lombok.experimental.FieldDefaults; + +@Data +@FieldDefaults(level = AccessLevel.PRIVATE) +public class DirectorUpdateRequest { + + @NotNull(message = "Id должен быть указан.") + @Positive(message = "Id должен быть положительным числом.") + Integer id; + + @Size(min = 2, max = 70, message = "Длина имени режиссера может быть от 2 до 70 символов.") + @NotBlank(message = "Имя режиссера не может быть пустым.") + String name; + + public boolean hasNname() { + return name != null; + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/request/update/FilmUpdateRequest.java b/src/main/java/ru/yandex/practicum/filmorate/dto/request/update/FilmUpdateRequest.java index 3f5c862..8fc4748 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dto/request/update/FilmUpdateRequest.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/request/update/FilmUpdateRequest.java @@ -6,11 +6,13 @@ import jakarta.validation.constraints.Size; import lombok.Data; import org.hibernate.validator.constraints.time.DurationMin; +import ru.yandex.practicum.filmorate.model.Director; import ru.yandex.practicum.filmorate.model.Genre; import ru.yandex.practicum.filmorate.model.Mpa; import java.time.Duration; import java.time.LocalDate; +import java.util.HashSet; import java.util.Set; @Data @@ -32,8 +34,9 @@ public class FilmUpdateRequest { @DurationMin(minutes = 1, message = "Продолжительность фильма должна быть положительным числом.") private Duration duration; - @Size(min = 1, message = "Должен быть указан хотябы один жанр.") - private Set genres; + private Set genres = new HashSet<>(); + + private Set directors = new HashSet<>(); private Mpa mpa; @@ -57,6 +60,10 @@ public boolean hasGenres() { return genres != null; } + public boolean hasDirectors() { + return directors != null; + } + public boolean hasMpa() { return mpa != null; } diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/request/update/ReviewUpdateRequest.java b/src/main/java/ru/yandex/practicum/filmorate/dto/request/update/ReviewUpdateRequest.java new file mode 100644 index 0000000..6b8dcc3 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/request/update/ReviewUpdateRequest.java @@ -0,0 +1,18 @@ +package ru.yandex.practicum.filmorate.dto.request.update; + +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Data +public class ReviewUpdateRequest { + @NotNull(message = "reviewId должен быть указан.") + private Long reviewId; + + @NotNull(message = "Контент отзыва не может быть пустым.") + @Size(max = 2048, message = "Максимальная длина отзыва — 2048 символов.") + private String content; + + @NotNull(message = "Тип отзыва должен быть указан.") + private Boolean isPositive; +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/request/update/UserUpdateRequest.java b/src/main/java/ru/yandex/practicum/filmorate/dto/request/update/UserUpdateRequest.java index 6276e1e..9740b9b 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dto/request/update/UserUpdateRequest.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/request/update/UserUpdateRequest.java @@ -8,36 +8,36 @@ @Data public class UserUpdateRequest { - @NotNull(message = "Id должен быть указан.") - @Positive(message = "Id должен быть положительным числом.") + @Positive(message = "Id должен быть положительным числом.") + @NotNull(message = "Id должен быть указан.") private Long id; - @Size(min = 2, max = 50, message = "Длина имени пользователя может быть от 2 до 50 символов.") - private String name; + @Size(max = 50, message = "Длина имени пользователя не может быть больше 50 символов.") + private String name; - @Pattern(regexp = "^[A-Za-z0-9_]+$", message = "Логин может состоять только из этих символов [A-Za-z0-9_].") - @Size(min = 3, max = 30, message = "Длина логина может быть от 3 до 20 символов.") + @Pattern(regexp = "^[A-Za-z0-9_]+$", message = "Логин может состоять только из этих символов [A-Za-z0-9_].") + @Size(min = 3, max = 30, message = "Длина логина может быть от 3 до 20 символов.") private String login; - @Email(message = "Не распознан формат адреса электронной почты.") - private String email; + @Email(message = "Не распознан формат адреса электронной почты.") + private String email; - @PastOrPresent(message = "Дата рождения не может быть в будущем.") - private LocalDate birthday; + @PastOrPresent(message = "Дата рождения не может быть в будущем.") + private LocalDate birthday; - public boolean hasNname() { - return name != null; - } + public boolean hasNname() { + return name != null; + } - public boolean hasLogin() { - return login != null; - } + public boolean hasLogin() { + return login != null; + } - public boolean hasEmail() { - return email != null; - } + public boolean hasEmail() { + return email != null; + } - public boolean hasBirthday() { - return birthday != null; - } + public boolean hasBirthday() { + return birthday != null; + } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/mapper/DirectorMapper.java b/src/main/java/ru/yandex/practicum/filmorate/mapper/DirectorMapper.java new file mode 100644 index 0000000..e27dbac --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/mapper/DirectorMapper.java @@ -0,0 +1,29 @@ +package ru.yandex.practicum.filmorate.mapper; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import ru.yandex.practicum.filmorate.dto.DirectorDto; +import ru.yandex.practicum.filmorate.dto.request.create.DirectorCreateRequest; +import ru.yandex.practicum.filmorate.dto.request.update.DirectorUpdateRequest; +import ru.yandex.practicum.filmorate.model.Director; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class DirectorMapper { + public static Director mapToDirector(DirectorCreateRequest request) { + return Director.builder() + .name(request.getName()) + .build(); + } + + public static DirectorDto mapToDirectorDto(Director director) { + return new DirectorDto(director.getId(), director.getName()); + } + + public static Director updateDirectorFields(Director director, DirectorUpdateRequest request) { + if (request.hasNname()) { + director.setName(request.getName()); + } + return director; + } + +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/mapper/EventMapper.java b/src/main/java/ru/yandex/practicum/filmorate/mapper/EventMapper.java new file mode 100644 index 0000000..943e78d --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/mapper/EventMapper.java @@ -0,0 +1,20 @@ +package ru.yandex.practicum.filmorate.mapper; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import ru.yandex.practicum.filmorate.dto.EventDto; +import ru.yandex.practicum.filmorate.model.Event; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class EventMapper { + public static EventDto mapToEventDto(Event event) { + return EventDto.builder() + .eventId(event.getEventId()) + .userId(event.getUserId()) + .timestamp(event.getTimestamp().toEpochMilli()) + .eventType(event.getEventType()) + .operation(event.getOperation()) + .entityId(event.getEntityId()) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/mapper/FilmMapper.java b/src/main/java/ru/yandex/practicum/filmorate/mapper/FilmMapper.java index 963e7f5..056a560 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/mapper/FilmMapper.java +++ b/src/main/java/ru/yandex/practicum/filmorate/mapper/FilmMapper.java @@ -2,11 +2,13 @@ import lombok.AccessLevel; import lombok.NoArgsConstructor; +import ru.yandex.practicum.filmorate.dto.DirectorDto; import ru.yandex.practicum.filmorate.dto.FilmDto; import ru.yandex.practicum.filmorate.dto.GenreDto; import ru.yandex.practicum.filmorate.dto.MpaDto; import ru.yandex.practicum.filmorate.dto.request.create.FilmCreateRequest; import ru.yandex.practicum.filmorate.dto.request.update.FilmUpdateRequest; +import ru.yandex.practicum.filmorate.model.Director; import ru.yandex.practicum.filmorate.model.Film; import ru.yandex.practicum.filmorate.model.Genre; import ru.yandex.practicum.filmorate.model.Mpa; @@ -28,10 +30,13 @@ public static Film mapToFilm(FilmCreateRequest request) { .genreIds(request.getGenres().stream() .map(Genre::getId) .collect(Collectors.toSet())) + .directorIds(request.getDirectors().stream() + .map(Director::getId) + .collect(Collectors.toSet())) .build(); } - public static FilmDto mapToFilmDto(Film film, Mpa mpa, Set genres) { + public static FilmDto mapToFilmDto(Film film, Mpa mpa, Set genres, Set directors) { return FilmDto.builder() .id(film.getId()) .name(film.getName()) @@ -44,6 +49,11 @@ public static FilmDto mapToFilmDto(Film film, Mpa mpa, Set genres) { .sorted(Comparator.comparing(Genre::getId)) .map(genre -> new GenreDto(genre.getId(), genre.getName())) .toList()) + .directors(directors.stream() + .sorted(Comparator.comparing(Director::getId)) + .map(director -> new DirectorDto(director.getId(), director.getName())) + .toList() + ) .build(); } @@ -69,11 +79,13 @@ public static Film updateFilmFields(Film film, FilmUpdateRequest request) { film.setMpaId(request.getMpa().getId()); } - if (request.hasGenres()) { - film.setGenreIds(request.getGenres().stream() - .map(Genre::getId) - .collect(Collectors.toSet())); - } + film.setGenreIds(request.getGenres().stream() + .map(Genre::getId) + .collect(Collectors.toSet())); + + film.setDirectorIds(request.getDirectors().stream() + .map(Director::getId) + .collect(Collectors.toSet())); return film; } diff --git a/src/main/java/ru/yandex/practicum/filmorate/mapper/ReviewMapper.java b/src/main/java/ru/yandex/practicum/filmorate/mapper/ReviewMapper.java new file mode 100644 index 0000000..547c3ef --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/mapper/ReviewMapper.java @@ -0,0 +1,42 @@ +package ru.yandex.practicum.filmorate.mapper; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import ru.yandex.practicum.filmorate.dto.ReviewDto; +import ru.yandex.practicum.filmorate.dto.request.create.ReviewCreateRequest; +import ru.yandex.practicum.filmorate.dto.request.update.ReviewUpdateRequest; +import ru.yandex.practicum.filmorate.model.Review; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class ReviewMapper { + + public static Review mapToReview(ReviewCreateRequest request) { + return Review.builder() + .content(request.getContent()) + .isPositive(request.getIsPositive()) + .userId(request.getUserId()) + .filmId(request.getFilmId()) + .build(); + } + + public static Review updateReviewFields(Review review, ReviewUpdateRequest request) { + if (request.getContent() != null) { + review.setContent(request.getContent()); + } + if (request.getIsPositive() != null) { + review.setIsPositive(request.getIsPositive()); + } + return review; + } + + public static ReviewDto mapToReviewDto(Review review) { + return ReviewDto.builder() + .reviewId(review.getId()) + .content(review.getContent()) + .isPositive(review.getIsPositive()) + .userId(review.getUserId()) + .filmId(review.getFilmId()) + .useful(review.getUseful()) + .build(); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/mapper/UserMapper.java b/src/main/java/ru/yandex/practicum/filmorate/mapper/UserMapper.java index 4bdfb96..78b40a6 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/mapper/UserMapper.java +++ b/src/main/java/ru/yandex/practicum/filmorate/mapper/UserMapper.java @@ -2,51 +2,51 @@ import lombok.AccessLevel; import lombok.NoArgsConstructor; +import ru.yandex.practicum.filmorate.dto.UserDto; import ru.yandex.practicum.filmorate.dto.request.create.UserCreateRequest; import ru.yandex.practicum.filmorate.dto.request.update.UserUpdateRequest; -import ru.yandex.practicum.filmorate.dto.UserDto; import ru.yandex.practicum.filmorate.model.User; @NoArgsConstructor(access = AccessLevel.PRIVATE) public final class UserMapper { - public static User mapToUser(UserCreateRequest request) { - return User.builder() - .name(request.getName() == null ? request.getLogin() : request.getName()) - .login(request.getLogin()) - .email(request.getEmail()) - .birthday(request.getBirthday()) - .build(); - } - - public static UserDto mapToUserDto(User user) { - return UserDto.builder() - .id(user.getId()) - .name(user.getName()) - .login(user.getLogin()) - .email(user.getEmail()) - .birthday(user.getBirthday()) - .friends(user.getFriendsIds()) - .build(); - } - - public static User updateUserFields(User user, UserUpdateRequest request) { - - if (request.hasNname()) { - user.setName(request.getName()); - } - - if (request.hasLogin()) { - user.setLogin(request.getLogin()); - } - - if (request.hasEmail()) { - user.setEmail(request.getEmail()); - } - - if (request.hasBirthday()) { - user.setBirthday(request.getBirthday()); - } - - return user; - } + public static User mapToUser(UserCreateRequest request) { + return User.builder() + .name(request.getName() == null ? request.getLogin() : request.getName()) + .login(request.getLogin()) + .email(request.getEmail()) + .birthday(request.getBirthday()) + .build(); + } + + public static UserDto mapToUserDto(User user) { + return UserDto.builder() + .id(user.getId()) + .name(user.getName()) + .login(user.getLogin()) + .email(user.getEmail()) + .birthday(user.getBirthday()) + .friends(user.getFriendsIds()) + .build(); + } + + public static User updateUserFields(User user, UserUpdateRequest request) { + + if (request.hasNname()) { + user.setName(request.getName()); + } + + if (request.hasLogin()) { + user.setLogin(request.getLogin()); + } + + if (request.hasEmail()) { + user.setEmail(request.getEmail()); + } + + if (request.hasBirthday()) { + user.setBirthday(request.getBirthday()); + } + + return user; + } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/Director.java b/src/main/java/ru/yandex/practicum/filmorate/model/Director.java new file mode 100644 index 0000000..b3b8659 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Director.java @@ -0,0 +1,18 @@ +package ru.yandex.practicum.filmorate.model; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.FieldDefaults; +import lombok.extern.jackson.Jacksonized; + +@Data +@Builder(toBuilder = true) +@EqualsAndHashCode(of = "id") +@FieldDefaults(level = AccessLevel.PRIVATE) +@Jacksonized +public class Director { + Integer id; + String name; +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/ErrorResponse.java b/src/main/java/ru/yandex/practicum/filmorate/model/ErrorResponse.java index 4145733..a6b0acb 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/ErrorResponse.java +++ b/src/main/java/ru/yandex/practicum/filmorate/model/ErrorResponse.java @@ -3,12 +3,10 @@ import lombok.Getter; public class ErrorResponse { - @Getter private final String error; public ErrorResponse(String error) { this.error = error; } - } diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/Event.java b/src/main/java/ru/yandex/practicum/filmorate/model/Event.java new file mode 100644 index 0000000..a033a30 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Event.java @@ -0,0 +1,21 @@ +package ru.yandex.practicum.filmorate.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.Instant; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Event { + private Long eventId; + private Long userId; + private Instant timestamp; + private EventType eventType; + private Operation operation; + private Long entityId; +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/EventType.java b/src/main/java/ru/yandex/practicum/filmorate/model/EventType.java new file mode 100644 index 0000000..e6cfb0b --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/EventType.java @@ -0,0 +1,7 @@ +package ru.yandex.practicum.filmorate.model; + +public enum EventType { + LIKE, + REVIEW, + FRIEND +} \ 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 a5d70aa..a16f4e6 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/Film.java +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Film.java @@ -13,8 +13,8 @@ @Data @Builder(toBuilder = true) -@Jacksonized @EqualsAndHashCode(of = "id") +@Jacksonized public class Film { private Long id; @@ -31,10 +31,13 @@ public class Film { private Integer mpaId; @Builder.Default - private Set likes = new HashSet<>(); + private Set directorIds = new HashSet<>(); @Builder.Default + private Set likes = new HashSet<>(); + @JsonProperty(value = "rate") + @Builder.Default private int likesCount = 0; public int getLikesCount() { @@ -54,6 +57,10 @@ public void addGenreId(int genreId) { genreIds.add(genreId); } + public void addDirectorId(int directorId) { + directorIds.add(directorId); + } + public void addLike(long userId) { likes.add(userId); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/Genre.java b/src/main/java/ru/yandex/practicum/filmorate/model/Genre.java index 2c953ef..3f89b27 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/Genre.java +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Genre.java @@ -10,5 +10,4 @@ public class Genre { private int id; private String name; - } diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/Operation.java b/src/main/java/ru/yandex/practicum/filmorate/model/Operation.java new file mode 100644 index 0000000..c0bc9bc --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Operation.java @@ -0,0 +1,7 @@ +package ru.yandex.practicum.filmorate.model; + +public enum Operation { + REMOVE, + ADD, + UPDATE +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/Review.java b/src/main/java/ru/yandex/practicum/filmorate/model/Review.java new file mode 100644 index 0000000..b32bff9 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Review.java @@ -0,0 +1,28 @@ +package ru.yandex.practicum.filmorate.model; + +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.extern.jackson.Jacksonized; + +import java.util.HashMap; +import java.util.Map; + +@Data +@Builder(toBuilder = true) +@EqualsAndHashCode(of = "id") +@Jacksonized +public class Review { + + private Long id; + private String content; + private Boolean isPositive; + private Long userId; + private Long filmId; + + @Builder.Default + private Long useful = 0L; + + @Builder.Default + private Map userLikes = new HashMap<>(); +} 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 6ac051b..414acce 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/User.java +++ b/src/main/java/ru/yandex/practicum/filmorate/model/User.java @@ -11,25 +11,24 @@ @Data @Builder(toBuilder = true) -@Jacksonized @EqualsAndHashCode(of = "id") +@Jacksonized public class User { - private Long id; - private String name; - private String login; - private String email; - private LocalDate birthday; - - @Builder.Default - private Set friendsIds = new HashSet<>(); + private Long id; + private String name; + private String login; + private String email; + private LocalDate birthday; - public void addFriendId(Long id) { - friendsIds.add(id); - } + @Builder.Default + private Set friendsIds = new HashSet<>(); - public void removeFriends(Long id) { - friendsIds.remove(id); - } + public void addFriendId(Long id) { + friendsIds.add(id); + } + public void removeFriends(Long id) { + friendsIds.remove(id); + } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/DirectorService.java b/src/main/java/ru/yandex/practicum/filmorate/service/DirectorService.java new file mode 100644 index 0000000..c2404b8 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/DirectorService.java @@ -0,0 +1,77 @@ +package ru.yandex.practicum.filmorate.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.dal.DirectorStorage; +import ru.yandex.practicum.filmorate.dto.DirectorDto; +import ru.yandex.practicum.filmorate.dto.request.create.DirectorCreateRequest; +import ru.yandex.practicum.filmorate.dto.request.update.DirectorUpdateRequest; +import ru.yandex.practicum.filmorate.exception.DuplicatedDataException; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.mapper.DirectorMapper; +import ru.yandex.practicum.filmorate.model.Director; + +import java.util.Collection; + +@Service +@RequiredArgsConstructor +@Slf4j +public class DirectorService { + private final DirectorStorage directorStorage; + + public DirectorDto getDirectorDtoById(int directorId) { + log.info("Получение режиссера с id={}", directorId); + return DirectorMapper.mapToDirectorDto(findDirector(directorId)); + } + + public Collection getAllDirectorDtos() { + log.info("Получение всех режиссеров"); + return directorStorage.findAll().stream().map(DirectorMapper::mapToDirectorDto).toList(); + } + + public DirectorDto create(DirectorCreateRequest request) { + log.info("Добавление нового режиссера name={}.", request.getName()); + Director director = DirectorMapper.mapToDirector(request); + + try { + director = directorStorage.createDirector(director); + } catch (DuplicateKeyException e) { + throw new DuplicatedDataException("Этот режиссер " + director.getName() + " уже был добавлен ранее."); + } + + return DirectorMapper.mapToDirectorDto(director); + } + + public DirectorDto update(DirectorUpdateRequest request) { + log.info("Обновление данных режиссера с id={}.", request.getId()); + Director oldDirector = findDirector(request.getId()); + Director newDirector = DirectorMapper.updateDirectorFields(oldDirector, request); + + try { + newDirector = directorStorage.updateDirector(newDirector); + } catch (DuplicateKeyException e) { + throw new DuplicatedDataException( + "Этот режиссер " + newDirector.getName() + " уже был добавлен ранее." + ); + } + + return DirectorMapper.mapToDirectorDto(newDirector); + } + + public void delete(int directorId) { + log.info("Удаление режиссера с id={}", directorId); + if (directorStorage.checkDirectorIsNotPresent(directorId)) { + throw new NotFoundException("Режиссер с id=" + directorId + " не найден."); + } + directorStorage.deleteDirector(directorId); + } + + private Director findDirector(int directorId) { + return directorStorage.findById(directorId).orElseThrow( + () -> new NotFoundException("Режиссер с id=" + directorId + " не найден.") + ); + } + +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/EventService.java b/src/main/java/ru/yandex/practicum/filmorate/service/EventService.java new file mode 100644 index 0000000..b50e86b --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/EventService.java @@ -0,0 +1,44 @@ +package ru.yandex.practicum.filmorate.service; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.dal.EventStorage; +import ru.yandex.practicum.filmorate.dto.EventDto; +import ru.yandex.practicum.filmorate.mapper.EventMapper; +import ru.yandex.practicum.filmorate.model.Event; +import ru.yandex.practicum.filmorate.model.EventType; +import ru.yandex.practicum.filmorate.model.Operation; + +import java.time.Instant; +import java.util.Collection; +import java.util.stream.Collectors; + +@Service +@Slf4j +public class EventService { + + @Qualifier("eventDbStorage") + @Autowired + private EventStorage eventStorage; + + public Event addUserEvent(long userId, EventType eventType, Operation operation, long entityId) { + Event event = Event.builder() + .userId(userId) + .timestamp(Instant.now()) + .eventType(eventType) + .operation(operation) + .entityId(entityId) + .build(); + return eventStorage.addEvent(event); + } + + public Collection getUserEvents(long userId) { + log.info("Поиск всех событий пользователя."); + return eventStorage.getAllEventsByUserId(userId) + .stream() + .map(EventMapper::mapToEventDto) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java b/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java new file mode 100644 index 0000000..569e0c5 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java @@ -0,0 +1,371 @@ +package ru.yandex.practicum.filmorate.service; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.dal.*; +import ru.yandex.practicum.filmorate.dto.FilmDto; +import ru.yandex.practicum.filmorate.dto.request.create.FilmCreateRequest; +import ru.yandex.practicum.filmorate.dto.request.update.FilmUpdateRequest; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.exception.ParameterNotValidException; +import ru.yandex.practicum.filmorate.exception.ValidationException; +import ru.yandex.practicum.filmorate.mapper.FilmMapper; +import ru.yandex.practicum.filmorate.model.*; +import ru.yandex.practicum.filmorate.service.util.FilmSortingAction; +import ru.yandex.practicum.filmorate.service.util.LikeAction; + +import java.util.*; + +@Service +@Slf4j +public class FilmService { + + private final FilmStorage filmStorage; + private final UserStorage userStorage; + private final MpaStorage mpaStorage; + private final GenreStorage genreStorage; + private final DirectorStorage directorStorage; + + private Map mpas; + private Map genres; + private final EventService eventService; + + @Autowired + public FilmService( + @Qualifier("filmDbStorage") FilmStorage filmStorage, + @Qualifier("userDbStorage") UserStorage userStorage, + @Qualifier("mpaDbStorage") MpaStorage mpaStorage, + @Qualifier("genreDbStorage") GenreStorage genreStorage, + @Qualifier("directorDbStorage") DirectorStorage directorStorage, + EventService eventService + ) { + this.filmStorage = filmStorage; + this.userStorage = userStorage; + this.mpaStorage = mpaStorage; + this.genreStorage = genreStorage; + this.directorStorage = directorStorage; + this.eventService = eventService; + } + + @EventListener(ApplicationReadyEvent.class) + public void initMpasAndGenres() { + this.mpas = mpaStorage.getMapOfAllMpa(); + this.genres = genreStorage.getMapOfAllGenres(); + } + + public Collection getSortedFilmsOfDirector(Integer directorId, String sortBy) { + log.info("Получение фильмов по режиссеру"); + if (directorId <= 0) { + throw new ParameterNotValidException(directorId.toString(), "должен быть положительным числом."); + } + + Collection films; + + if (sortBy == null) { + return getSortedFilmDtosOfDirector(directorId).stream() + .map(film -> FilmMapper.mapToFilmDto( + film, + mpas.get(film.getMpaId()), + getGenresByIds(film.getGenreIds()), + getDirectorsByIds(film.getDirectorIds()) + ) + ).toList(); + } + + log.trace("Проверка сортировочного запроса"); + FilmSortingAction sortingAction = FilmSortingAction.of(sortBy); + + log.trace("Применение сортировки"); + Comparator comparator = switch (sortingAction) { + case YEAR -> Comparator.comparing(Film::getReleaseDate); + case LIKES -> Comparator.comparing(Film::getLikesCount).reversed(); + case UNDEFINED -> throw new ParameterNotValidException( + sortingAction.getSortingName(), + "не подходит. Допустимые значения: " + + FilmSortingAction.getValidSortingList() + ); + }; + + films = getSortedFilmDtosOfDirector(directorId).stream() + .sorted(comparator).toList(); + + return films.stream() + .map(film -> FilmMapper.mapToFilmDto( + film, + mpas.get(film.getMpaId()), + getGenresByIds(film.getGenreIds()), + getDirectorsByIds(film.getDirectorIds()) + ) + ).toList(); + } + + private Collection getSortedFilmDtosOfDirector(Integer directorId) { + Collection films = filmStorage.getALLFilmsOfDirector(directorId); + + if (films.isEmpty()) { + throw new NotFoundException("Фильмы с этим режиссером" + + " найдены, попробуйте изменить режиссера или параметры сортировки"); + } + + return films; + } + + public void changeLike(LikeAction action, Long filmId, Long userId) { + switch (action) { + case SET -> log.info("Добавление лайка пользователя id={} к фильму id={}.", userId, filmId); + case REMOVE -> log.info("Удаление лайка пользователя id={} у фильма id={}.", userId, filmId); + } + + if (filmStorage.checkFilmIsNotPresent(filmId)) { + throw new NotFoundException("Фильм с id=" + filmId + " не найден."); + } + + if (userStorage.checkUserIsNotPresent(userId)) { + throw new NotFoundException("Пользователь с id=" + userId + " не найден."); + } + + switch (action) { + case SET -> { + filmStorage.setLike(filmId, userId); + eventService.addUserEvent(userId, EventType.LIKE, Operation.ADD, filmId); + } + case REMOVE -> { + filmStorage.removeLike(filmId, userId); + eventService.addUserEvent(userId, EventType.LIKE, Operation.REMOVE, filmId); + } + } + } + + public Collection getTopFilms(Integer count, Integer genreId, String year) { + log.info("Получение топ {} фильмов.", count); + if (count <= 0) { + throw new ParameterNotValidException( + String.valueOf(count), + "Топ фильмов должен быть положительным числом." + ); + } + + Collection films; + if (genreId != null || year != null) { + films = filmStorage.getTopByFilters(count, genreId, year); + } else { + films = filmStorage.getTop(count); + } + + return films.stream() + .map(film -> FilmMapper.mapToFilmDto( + film, + mpas.get(film.getMpaId()), + getGenresByIds(film.getGenreIds()), + getDirectorsByIds(film.getDirectorIds()) + ) + ).sorted(Comparator.comparing(FilmDto::getLikesCount).reversed()).toList(); + } + + public Collection findAll() { + log.info("Поиск всех фильмов"); + return filmStorage.findAll().stream() + .map(film -> FilmMapper.mapToFilmDto( + film, + mpas.get(film.getMpaId()), + getGenresByIds(film.getGenreIds()), + getDirectorsByIds(film.getDirectorIds()) + ) + ).toList(); + } + + public FilmDto findById(Long id) { + log.info("Получение фильма с id={}", id); + return filmStorage.findById(id) + .map(film -> FilmMapper.mapToFilmDto( + film, + mpas.get(film.getMpaId()), + getGenresByIds(film.getGenreIds()), + getDirectorsByIds(film.getDirectorIds()) + ) + ) + .orElseThrow(() -> new NotFoundException("Фильм с id=" + id + " не найден.") + ); + } + + public Collection getCommonLikedFilms(Long userId, Long friendId) { + log.info("Получение общих с другом фильмов userId={}, friendId={}", userId, friendId); + if (Objects.equals(userId, friendId)) { + throw new ValidationException("У пользователей должны быть разные id."); + } + if (userStorage.checkUserIsNotPresent(userId)) { + throw new NotFoundException("Пользователь с id=" + userId + " не найден."); + } + if (userStorage.checkUserIsNotPresent(friendId)) { + throw new NotFoundException("Пользователь с id=" + friendId + " не найден."); + } + return filmStorage.getCommonLikedFilms(userId, friendId).stream() + .map(film -> FilmMapper.mapToFilmDto( + film, + mpas.get(film.getMpaId()), + getGenresByIds(film.getGenreIds()), + getDirectorsByIds(film.getDirectorIds()) + ) + ).toList(); + } + + public FilmDto create(FilmCreateRequest request) { + log.info("Добавление нового фильма"); + + Film film = FilmMapper.mapToFilm(request); + Mpa mpa = validateMpa(film); + Set filmGenres = validateGenres(film); + Set filmDirectors = getDirectorsByIds(film.getDirectorIds()); + + film = filmStorage.createFilm(film); + + return FilmMapper.mapToFilmDto(film, mpa, filmGenres, filmDirectors); + } + + public FilmDto update(FilmUpdateRequest request) { + log.info("Обновление данных о фильме с id={}", request.getId()); + + Film oldFilm = filmStorage.findById(request.getId()) + .orElseThrow(() -> new NotFoundException("Фильм с id=" + request.getId() + " не найден.")); + + logFilmUpdate(request, oldFilm); + + Film newFilm = FilmMapper.updateFilmFields(oldFilm, request); + Mpa mpa = validateMpa(newFilm); + Set filmGenres = validateGenres(newFilm); + Set filmDirectors = getDirectorsByIds(newFilm.getDirectorIds()); + newFilm = filmStorage.updateFilm(newFilm); + + return FilmMapper.mapToFilmDto(newFilm, mpa, filmGenres, filmDirectors); + } + + public void remove(long filmId) { + log.info("Удаление фильма с id={}", filmId); + if (filmStorage.checkFilmIsNotPresent(filmId)) { + throw new NotFoundException("Фильм с id=" + filmId + " не найден."); + } + filmStorage.removeFilm(filmId); + } + + private Mpa validateMpa(Film film) { + log.trace("Проверка рейтинга MPAA"); + int mpaId = film.getMpaId(); + return mpaStorage.getMpaById(mpaId).orElseThrow(() -> + new NotFoundException("Рейтинг MPAA с id=" + mpaId + " не найден.")); + } + + private Set validateGenres(Film film) { + log.trace("Проверка жанров"); + Set requestGenreIds = film.getGenreIds(); + List badGenreids = requestGenreIds.stream() + .filter(id -> !genres.containsKey(id)) + .sorted().toList(); + if (!badGenreids.isEmpty()) { + throw new NotFoundException("Жанры с id " + badGenreids + " не найдены."); + } + return getGenresByIds(requestGenreIds); + } + + private Set getGenresByIds(Set genreIds) { + HashSet gnrs = new HashSet<>(); + for (Integer id : genreIds) { + if (genres.containsKey(id)) { + gnrs.add(genres.get(id)); + } + } + return gnrs; + } + + private Set getDirectorsByIds(Set directorIds) { + Map allDirectorIds = new HashMap<>(); + directorStorage.findAll() + .forEach(director -> allDirectorIds.put(director.getId(), director)); + + log.trace("Проверка режиссеров"); + List badDirectorIds = directorIds.stream() + .filter(id -> !allDirectorIds.containsKey(id)) + .toList(); + if (!badDirectorIds.isEmpty()) { + throw new NotFoundException("Режиссеры с id " + badDirectorIds + " не найдены."); + } + + HashSet dirs = new HashSet<>(); + for (Integer id : directorIds) { + if (allDirectorIds.containsKey(id)) { + dirs.add(allDirectorIds.get(id)); + } + } + return dirs; + } + + private void logFilmUpdate(FilmUpdateRequest request, Film film) { + List updatedFields = new ArrayList<>(); + + if (request.hasName() && !film.getName().equals(request.getName())) { + updatedFields.add("название"); + } + if (request.hasDescription() && !film.getDescription().equals(request.getDescription())) { + updatedFields.add("описание"); + } + if (request.hasReleaseDate() && !film.getReleaseDate().equals(request.getReleaseDate())) { + updatedFields.add("дата релиза"); + } + if (request.hasDuration() && !film.getDuration().minus(request.getDuration()).isZero()) { + updatedFields.add("продолжительность"); + } + if (request.hasMpa() && !film.getMpaId().equals(request.getMpa().getId())) { + updatedFields.add("рейтинг MPAA"); + } + if (request.hasGenres() && !request.getGenres().stream() + .map(Genre::getId).allMatch(film.getGenreIds()::contains) + ) { + updatedFields.add("жанры"); + } + if (request.hasDirectors() && !request.getDirectors().stream() + .map(Director::getId).allMatch(film.getDirectorIds()::contains) + ) { + updatedFields.add("режиссеры"); + } + + String updatedData = String.join(", ", updatedFields); + log.info("Данные для обновления: [{}]", updatedData); + } + + public Collection search(String query, String by) { + log.info("Запрос на поиск фильмов: query='{}', by='{}'", query, by); + if (query == null || query.isBlank()) { + throw new ParameterNotValidException("query", "Поисковая строка не может быть пустой."); + } + if (by == null || by.isBlank()) { + throw new ParameterNotValidException("by", + "Параметр поля для поиска должен содержать 'title' или 'director'"); + } + + return filmStorage.search(query, by).stream() + .map(film -> FilmMapper.mapToFilmDto( + film, + mpas.get(film.getMpaId()), + getGenresByIds(film.getGenreIds()), + getDirectorsByIds(film.getDirectorIds()) + )) + .toList(); + } + + public Collection getFilmsByIds(Collection ids) { + if (ids == null || ids.isEmpty()) { + return Collections.emptyList(); + } + return filmStorage.getFilmsByIds(ids).stream() + .map(film -> FilmMapper.mapToFilmDto( + film, + mpas.get(film.getMpaId()), + getGenresByIds(film.getGenreIds()), + getDirectorsByIds(film.getDirectorIds()) + )) + .toList(); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/servise/GenreService.java b/src/main/java/ru/yandex/practicum/filmorate/service/GenreService.java similarity index 83% rename from src/main/java/ru/yandex/practicum/filmorate/servise/GenreService.java rename to src/main/java/ru/yandex/practicum/filmorate/service/GenreService.java index a141457..20c2973 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/servise/GenreService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/GenreService.java @@ -1,4 +1,4 @@ -package ru.yandex.practicum.filmorate.servise; +package ru.yandex.practicum.filmorate.service; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -18,14 +18,14 @@ public class GenreService { public GenreDto getGenreDtoById(long genreId) { log.info("Получение жанра с id={}", genreId); return genreStorage.getGenreById(genreId) - .map(genre -> new GenreDto(genre.getId(), genre.getName())) + .map(genre -> new GenreDto(genre.getId(), genre.getName())) .orElseThrow(() -> new NotFoundException("Жанр с id=" + genreId + " не найден.")); } public Collection getAllGenreDtos() { log.info("Получение всех жанров"); return genreStorage.getAllGenres().stream() - .map(genre -> new GenreDto(genre.getId(), genre.getName())) + .map(genre -> new GenreDto(genre.getId(), genre.getName())) .toList(); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/servise/MpaServise.java b/src/main/java/ru/yandex/practicum/filmorate/service/MpaService.java similarity index 81% rename from src/main/java/ru/yandex/practicum/filmorate/servise/MpaServise.java rename to src/main/java/ru/yandex/practicum/filmorate/service/MpaService.java index 00b231d..60b7605 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/servise/MpaServise.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/MpaService.java @@ -1,4 +1,4 @@ -package ru.yandex.practicum.filmorate.servise; +package ru.yandex.practicum.filmorate.service; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -9,24 +9,24 @@ import java.util.Collection; -@Slf4j @Service @RequiredArgsConstructor -public class MpaServise { +@Slf4j +public class MpaService { private final MpaStorage mpaStorage; public MpaDto getMpaDtoById(long mpaId) { log.info("Получение рейтинга MPAA с id={}", mpaId); return mpaStorage.getMpaById(mpaId) - .map(mpa -> new MpaDto(mpa.getId(), mpa.getName())) + .map(mpa -> new MpaDto(mpa.getId(), mpa.getName())) .orElseThrow(() -> new NotFoundException("Рейтинг MPAA с id=" + mpaId + " не найден.")); } public Collection getAllMpaDtos() { log.info("Получение всех рейтингов MPAA"); return mpaStorage.getAllMpa().stream() - .map(mpa -> new MpaDto(mpa.getId(), mpa.getName())) + .map(mpa -> new MpaDto(mpa.getId(), mpa.getName())) .toList(); } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/ReviewService.java b/src/main/java/ru/yandex/practicum/filmorate/service/ReviewService.java new file mode 100644 index 0000000..2dd323c --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/ReviewService.java @@ -0,0 +1,153 @@ +package ru.yandex.practicum.filmorate.service; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.dal.FilmStorage; +import ru.yandex.practicum.filmorate.dal.ReviewStorage; +import ru.yandex.practicum.filmorate.dal.UserStorage; +import ru.yandex.practicum.filmorate.dto.ReviewDto; +import ru.yandex.practicum.filmorate.dto.request.create.ReviewCreateRequest; +import ru.yandex.practicum.filmorate.dto.request.update.ReviewUpdateRequest; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.mapper.ReviewMapper; +import ru.yandex.practicum.filmorate.model.EventType; +import ru.yandex.practicum.filmorate.model.Operation; +import ru.yandex.practicum.filmorate.model.Review; + +import java.util.Collection; +import java.util.Optional; + +@Service +@Slf4j +public class ReviewService { + + private final ReviewStorage reviewStorage; + private final FilmStorage filmStorage; + private final UserStorage userStorage; + private final EventService eventService; + + @Autowired + public ReviewService( + @Qualifier("reviewDbStorage") ReviewStorage reviewStorage, + @Qualifier("filmDbStorage") FilmStorage filmStorage, + @Qualifier("userDbStorage") UserStorage userStorage, + EventService eventService + ) { + this.reviewStorage = reviewStorage; + this.filmStorage = filmStorage; + this.userStorage = userStorage; + this.eventService = eventService; + } + + public ReviewDto createReview(ReviewCreateRequest request) { + log.info("Добавление нового отзыва пользователем id={} для фильма id={}", request.getUserId(), request.getFilmId()); + + checkUserExists(request.getUserId()); + checkFilmExists(request.getFilmId()); + + Review review = ReviewMapper.mapToReview(request); + review = reviewStorage.createReview(review); + eventService.addUserEvent(review.getUserId(), EventType.REVIEW, Operation.ADD, review.getId()); + + return ReviewMapper.mapToReviewDto(review); + } + + public ReviewDto updateReview(ReviewUpdateRequest request) { + log.info("Обновление данных об отзыве с id={}", request.getReviewId()); + Review oldReview = findReview(request.getReviewId()); + Review newReview = ReviewMapper.updateReviewFields(oldReview, request); + newReview = reviewStorage.updateReview(newReview); + eventService.addUserEvent(newReview.getUserId(), EventType.REVIEW, Operation.UPDATE, newReview.getId()); + + return ReviewMapper.mapToReviewDto(newReview); + } + + public void deleteReview(Long id) { + log.info("Удаление отзыва с id={}", id); + checkReviewExists(id); + long userId = findReviewById(id).getUserId(); + reviewStorage.deleteReview(id); + eventService.addUserEvent(userId, EventType.REVIEW, Operation.REMOVE, id); + } + + public ReviewDto findReviewById(Long id) { + log.info("Получение отзыва с id={}", id); + return ReviewMapper.mapToReviewDto(findReview(id)); + } + + public Collection findReviewsByFilmId(Long filmId, int count) { + log.info("Получение отзывов для фильма с id={} в количестве {}", filmId, count); + return reviewStorage.findReviewsByFilmId(filmId, count).stream() + .map(ReviewMapper::mapToReviewDto) + .toList(); + } + + public void addReviewLike(Long id, Long userId) { + log.info("Пользователь id={} ставит лайк отзыву id={}", userId, id); + checkReviewExists(id); + checkUserExists(userId); + + Optional likeStatus = reviewStorage.getReactionsStatus(id, userId); + + if (likeStatus.isEmpty()) { + reviewStorage.addReviewLike(id, userId); + } else if (!likeStatus.get()) { + reviewStorage.removeReviewDislike(id, userId); + reviewStorage.addReviewLike(id, userId); + } + } + + public void addReviewDislike(Long id, Long userId) { + log.info("Пользователь id={} ставит дизлайк отзыву id={}", userId, id); + checkReviewExists(id); + checkUserExists(userId); + Optional likeStatus = reviewStorage.getReactionsStatus(id, userId); + + if (likeStatus.isEmpty()) { + reviewStorage.addReviewDislike(id, userId); + } else if (likeStatus.get()) { + reviewStorage.removeReviewLike(id, userId); + reviewStorage.addReviewDislike(id, userId); + } + + } + + public void removeReviewLike(Long id, Long userId) { + log.info("Пользователь id={} удаляет лайк у отзыва id={}", userId, id); + checkReviewExists(id); + checkUserExists(userId); + reviewStorage.removeReviewLike(id, userId); + } + + public void removeReviewDislike(Long id, Long userId) { + log.info("Пользователь id={} удаляет дизлайк у отзыва id={}", userId, id); + checkReviewExists(id); + checkUserExists(userId); + reviewStorage.removeReviewDislike(id, userId); + } + + private Review findReview(long reviewId) { + return reviewStorage.findReviewById(reviewId) + .orElseThrow(() -> new NotFoundException("Отзыв с id=" + reviewId + " не найден.")); + } + + private void checkFilmExists(Long filmId) { + if (filmStorage.checkFilmIsNotPresent(filmId)) { + throw new NotFoundException("Фильм с id=" + filmId + " не найден."); + } + } + + private void checkUserExists(Long userId) { + if (userStorage.checkUserIsNotPresent(userId)) { + throw new NotFoundException("Пользователь с id=" + userId + " не найден."); + } + } + + private void checkReviewExists(Long id) { + if (reviewStorage.checkReviewIsNotPresent(id)) { + throw new NotFoundException("Отзыв с id=" + id + " не найден."); + } + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/servise/UserService.java b/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java similarity index 66% rename from src/main/java/ru/yandex/practicum/filmorate/servise/UserService.java rename to src/main/java/ru/yandex/practicum/filmorate/service/UserService.java index 7c736e6..aa2485f 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/servise/UserService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java @@ -1,22 +1,25 @@ -package ru.yandex.practicum.filmorate.servise; +package ru.yandex.practicum.filmorate.service; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.dao.DuplicateKeyException; import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.dal.FilmStorage; import ru.yandex.practicum.filmorate.dal.UserStorage; -import ru.yandex.practicum.filmorate.dto.request.create.UserCreateRequest; +import ru.yandex.practicum.filmorate.dto.EventDto; +import ru.yandex.practicum.filmorate.dto.FilmDto; import ru.yandex.practicum.filmorate.dto.UserDto; +import ru.yandex.practicum.filmorate.dto.request.create.UserCreateRequest; import ru.yandex.practicum.filmorate.dto.request.update.UserUpdateRequest; import ru.yandex.practicum.filmorate.exception.*; import ru.yandex.practicum.filmorate.mapper.UserMapper; +import ru.yandex.practicum.filmorate.model.EventType; +import ru.yandex.practicum.filmorate.model.Operation; import ru.yandex.practicum.filmorate.model.User; -import ru.yandex.practicum.filmorate.servise.util.FriendsAction; +import ru.yandex.practicum.filmorate.service.util.FriendsAction; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; +import java.util.*; import java.util.stream.Collectors; @Service @@ -27,6 +30,16 @@ public class UserService { @Qualifier("userDbStorage") private UserStorage userStorage; + @Autowired + @Qualifier("filmDbStorage") + private FilmStorage filmStorage; + + @Autowired + private EventService eventService; + + @Autowired + private FilmService filmService; + public void changeFriends(FriendsAction action, long userId, long friendId) { switch (action) { case REQUEST -> log.info( @@ -55,24 +68,29 @@ public void changeFriends(FriendsAction action, long userId, long friendId) { } if (riendsOfFriend.contains(userId)) { - userStorage.addFriend(userId, friendId); log.info("Пользователи с id={} и id={} стали друзьями.", userId, friendId); } userStorage.addFriend(userId, friendId); + eventService.addUserEvent(userId, EventType.FRIEND, Operation.ADD, friendId); } case REMOVE -> { - if (! friendsOfUser.contains(friendId)) { + if (!friendsOfUser.contains(friendId)) { throw new NoContentException(); } userStorage.removeFriend(userId, friendId); + eventService.addUserEvent(userId, EventType.FRIEND, Operation.REMOVE, friendId); } } } public UserDto create(UserCreateRequest request) { - log.info("Добавление нового пользователя login={}.", request.getLogin()); + log.info("Добавление нового пользователя login={}.", request.getLogin()); + if (request.getName().isBlank()) { + request.setName(request.getLogin()); + } + User user = UserMapper.mapToUser(request); try { @@ -86,6 +104,10 @@ public UserDto create(UserCreateRequest request) { public UserDto update(UserUpdateRequest request) { log.info("Обновление данных о пользователе с id={}.", request.getId()); + if (request.getName().isBlank()) { + request.setName(request.getLogin()); + } + User oldUser = findUser(request.getId()); logUserUpdate(request, oldUser); User newUser = UserMapper.updateUserFields(oldUser, request); @@ -101,6 +123,12 @@ public UserDto update(UserUpdateRequest request) { return UserMapper.mapToUserDto(newUser); } + public void remove(long userId) { + log.info("Удаление пользователя с id={}.", userId); + easyCheckUser(userId); + userStorage.removeUser(userId); + } + public UserDto findUserDtoById(long userId) { log.info("Поиск пользователя с id={}.", userId); return UserMapper.mapToUserDto(findUser(userId)); @@ -132,6 +160,12 @@ public Collection getListOfMutualFriends(long userId, long friendId) { .toList(); } + public Collection getUserEvents(long userId) { + log.info("Получение событий пользователя с id={}.", userId); + easyCheckUser(userId); + return eventService.getUserEvents(userId); + } + private void easyCheckUser(long userId) { if (userStorage.checkUserIsNotPresent(userId)) { throw new NotFoundException("Первый пользователь с id=" + userId + " не найден."); @@ -171,4 +205,56 @@ private void logUserUpdate(UserUpdateRequest request, User user) { String updatedData = String.join(", ", updatedFields); log.info("Данные для обновления: [{}]", updatedData); } + + public Collection getRecommendations(long userId) { + log.info("Получение рекомендаций для пользователя id={}.", userId); + easyCheckUser(userId); + + Map> allLikes = filmStorage.getAllLikes(); + Set targetUserLikes = allLikes.getOrDefault(userId, Collections.emptySet()); + + if (targetUserLikes.isEmpty()) { + return Collections.emptyList(); + } + + Map similarityMap = new HashMap<>(); + + for (Map.Entry> entry : allLikes.entrySet()) { + long otherUserId = entry.getKey(); + if (otherUserId == userId) continue; + + Set otherUserLikes = entry.getValue(); + int overlap = (int) targetUserLikes.stream() + .filter(otherUserLikes::contains) + .count(); + + if (overlap > 0) { + similarityMap.put(otherUserId, overlap); + } + } + + if (similarityMap.isEmpty()) { + return Collections.emptyList(); + } + + List similarUserIds = similarityMap.entrySet().stream() + .sorted(Map.Entry.comparingByValue().reversed()) + .limit(15) + .map(Map.Entry::getKey) + .toList(); + + Set recommendedFilmIds = new HashSet<>(); + for (Long simId : similarUserIds) { + Set likedBySimilarUser = allLikes.get(simId); + likedBySimilarUser.stream() + .filter(filmId -> !targetUserLikes.contains(filmId)) + .forEach(recommendedFilmIds::add); + } + + if (recommendedFilmIds.isEmpty()) { + return Collections.emptyList(); + } + + return filmService.getFilmsByIds(recommendedFilmIds); + } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/util/FilmSortingAction.java b/src/main/java/ru/yandex/practicum/filmorate/service/util/FilmSortingAction.java new file mode 100644 index 0000000..4803a27 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/util/FilmSortingAction.java @@ -0,0 +1,43 @@ +package ru.yandex.practicum.filmorate.service.util; + +import lombok.Getter; + +import java.util.Arrays; +import java.util.List; + +public enum FilmSortingAction { + YEAR, LIKES, UNDEFINED; + + @Getter + private String sortingName; + + public static FilmSortingAction of(String value) { + FilmSortingAction filterAction; + for (FilmSortingAction ffa : values()) { + if (ffa.name().equalsIgnoreCase(value)) { + return ffa; + } + } + + filterAction = FilmSortingAction.UNDEFINED; + filterAction.setSortingName(value); + return filterAction; + } + + private void setSortingName(String sortingName) { + this.sortingName = sortingName; + } + + public boolean isUndefined() { + return this == UNDEFINED; + } + + public static List getValidSortingList() { + return Arrays.stream(FilmSortingAction.values()) + .filter(f -> !f.equals(FilmSortingAction.UNDEFINED)) + .map(FilmSortingAction::name) + .map(String::toLowerCase) + .toList(); + } + +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/servise/util/FriendsAction.java b/src/main/java/ru/yandex/practicum/filmorate/service/util/FriendsAction.java similarity index 55% rename from src/main/java/ru/yandex/practicum/filmorate/servise/util/FriendsAction.java rename to src/main/java/ru/yandex/practicum/filmorate/service/util/FriendsAction.java index ef1ffc5..36e31c3 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/servise/util/FriendsAction.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/util/FriendsAction.java @@ -1,4 +1,4 @@ -package ru.yandex.practicum.filmorate.servise.util; +package ru.yandex.practicum.filmorate.service.util; public enum FriendsAction { REQUEST, // GET diff --git a/src/main/java/ru/yandex/practicum/filmorate/servise/util/IdentifyService.java b/src/main/java/ru/yandex/practicum/filmorate/service/util/IdentifyService.java similarity index 77% rename from src/main/java/ru/yandex/practicum/filmorate/servise/util/IdentifyService.java rename to src/main/java/ru/yandex/practicum/filmorate/service/util/IdentifyService.java index e6b34a5..6b85448 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/servise/util/IdentifyService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/util/IdentifyService.java @@ -1,14 +1,14 @@ -package ru.yandex.practicum.filmorate.servise.util; +package ru.yandex.practicum.filmorate.service.util; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.util.Map; -@Slf4j @Service +@Slf4j public class IdentifyService { - public long getNextId(Map map) { + public long getNextId(Map map) { log.trace("Генерация нового id"); int currentMaxId = map.keySet() .stream() diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/util/LikeAction.java b/src/main/java/ru/yandex/practicum/filmorate/service/util/LikeAction.java new file mode 100644 index 0000000..dd910e2 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/util/LikeAction.java @@ -0,0 +1,5 @@ +package ru.yandex.practicum.filmorate.service.util; + +public enum LikeAction { + SET, REMOVE +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/servise/FilmService.java b/src/main/java/ru/yandex/practicum/filmorate/servise/FilmService.java deleted file mode 100644 index 7656d9c..0000000 --- a/src/main/java/ru/yandex/practicum/filmorate/servise/FilmService.java +++ /dev/null @@ -1,204 +0,0 @@ -package ru.yandex.practicum.filmorate.servise; - -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.boot.context.event.ApplicationReadyEvent; -import org.springframework.context.event.EventListener; -import org.springframework.stereotype.Service; -import ru.yandex.practicum.filmorate.dal.FilmStorage; -import ru.yandex.practicum.filmorate.dal.GenreStorage; -import ru.yandex.practicum.filmorate.dal.MpaStorage; -import ru.yandex.practicum.filmorate.dal.UserStorage; -import ru.yandex.practicum.filmorate.dto.FilmDto; -import ru.yandex.practicum.filmorate.dto.request.create.FilmCreateRequest; -import ru.yandex.practicum.filmorate.dto.request.update.FilmUpdateRequest; -import ru.yandex.practicum.filmorate.exception.NotFoundException; -import ru.yandex.practicum.filmorate.exception.ParameterNotValidException; -import ru.yandex.practicum.filmorate.mapper.FilmMapper; -import ru.yandex.practicum.filmorate.model.Film; -import ru.yandex.practicum.filmorate.model.Genre; -import ru.yandex.practicum.filmorate.model.Mpa; -import ru.yandex.practicum.filmorate.servise.util.LikeAction; - -import java.util.*; - -@Service -@Slf4j -public class FilmService { - - private final FilmStorage filmStorage; - private final UserStorage userStorage; - private final MpaStorage mpaStorage; - private final GenreStorage genreStorage; - - private Map mpas; - private Map genres; - - @Autowired - public FilmService( - @Qualifier("filmDbStorage") FilmStorage filmStorage, - @Qualifier("userDbStorage") UserStorage userStorage, - @Qualifier("mpaDbStorage") MpaStorage mpaStorage, - @Qualifier("genreDbStorage") GenreStorage genreStorage - ) { - this.filmStorage = filmStorage; - this.userStorage = userStorage; - this.mpaStorage = mpaStorage; - this.genreStorage = genreStorage; - } - - @EventListener(ApplicationReadyEvent.class) - public void initMpasAndGenres() { - this.mpas = mpaStorage.getMapOfAllMpa(); - this.genres = genreStorage.getMapOfAllGenres(); - } - - public void changeLike(LikeAction action, Long filmId, Long userId) { - switch (action) { - case SET -> log.info("Добавление лайка пользователя id={} к фильму id={}.", userId, filmId); - case REMOVE -> log.info("Удаление лайка пользователя id={} у фильма id={}.", userId, filmId); - } - - if (filmStorage.checkFilmIsNotPresent(filmId)) { - throw new NotFoundException("Фильм с id=" + filmId + " не найден."); - } - - if (userStorage.checkUserIsNotPresent(userId)) { - throw new NotFoundException("Пользователь с id=" + userId + " не найден."); - } - - switch (action) { - case SET -> filmStorage.setLike(filmId, userId); - case REMOVE -> filmStorage.removeLike(filmId, userId); - } - } - - public Collection getTopFilms(Integer count) { - log.info("Получение топ {} фильмов.", count); - if (count <= 0) { - throw new ParameterNotValidException( - String.valueOf(count), - "Топ фильмов должен быть положительным числом." - ); - } - return filmStorage.getTop(count).stream() - .map(film -> FilmMapper.mapToFilmDto( - film, - mpas.get(film.getMpaId()), - getGenresByIds(film.getGenreIds()) - ) - ).sorted(Comparator.comparing(FilmDto::getLikesCount).reversed()).toList(); - } - - public Collection findAll() { - log.info("Поиск всех фильмов"); - return filmStorage.findAll().stream() - .map(film -> FilmMapper.mapToFilmDto( - film, - mpas.get(film.getMpaId()), - getGenresByIds(film.getGenreIds()) - ) - ).toList(); - } - - public FilmDto findById(Long id) { - log.info("Получение фильма с id={}", id); - return filmStorage.findById(id) - .map(film -> FilmMapper.mapToFilmDto( - film, - mpas.get(film.getMpaId()), - getGenresByIds(film.getGenreIds()) - ) - ) - .orElseThrow(() -> new NotFoundException("Фильм с id=" + id + " не найден.") - ); - } - - public FilmDto create(FilmCreateRequest request) { - log.info("Добавление нового фильма"); - - Film film = FilmMapper.mapToFilm(request); - Mpa mpa = validateMpa(film); - Set filmGenres = validateGenres(film); - - film = filmStorage.createFilm(film); - - return FilmMapper.mapToFilmDto(film, mpa, filmGenres); - } - - public FilmDto update(FilmUpdateRequest request) { - log.info("Обновление данных о фильме с id={}", request.getId()); - - Film oldFilm = filmStorage.findById(request.getId()) - .orElseThrow(() -> new NotFoundException("Фильм с id=" + request.getId() + " не найден.")); - - logFilmUpdate(request, oldFilm); - - Film newFilm = FilmMapper.updateFilmFields(oldFilm, request); - Mpa mpa = validateMpa(newFilm); - Set filmGenres = validateGenres(newFilm); - newFilm = filmStorage.updateFilm(newFilm); - - return FilmMapper.mapToFilmDto(newFilm, mpa, filmGenres); - } - - private Mpa validateMpa(Film film) { - log.trace("Проверка рейтинга MPAA"); - int mpaId = film.getMpaId(); - return mpaStorage.getMpaById(mpaId).orElseThrow(() -> - new NotFoundException("Рейтинг MPAA с id=" + mpaId + " не найден.")); - } - - private Set validateGenres(Film film) { - log.trace("Проверка жанров"); - Set requestGenreIds = film.getGenreIds(); - List badGenreids = requestGenreIds.stream() - .filter(id -> !genres.containsKey(id)) - .sorted().toList(); - if (!badGenreids.isEmpty()) { - throw new NotFoundException("Жанры с id " + badGenreids + " не найдены."); - } - return getGenresByIds(requestGenreIds); - } - - private Set getGenresByIds(Set genreIds) { - HashSet gnrs = new HashSet<>(); - for (Integer id : genreIds) { - if (genres.containsKey(id)) { - gnrs.add(genres.get(id)); - } - } - return gnrs; - } - - private void logFilmUpdate(FilmUpdateRequest request, Film film) { - List updatedFields = new ArrayList<>(); - - if (request.hasName() && !film.getName().equals(request.getName())) { - updatedFields.add("название"); - } - if (request.hasDescription() && !film.getDescription().equals(request.getDescription())) { - updatedFields.add("описание"); - } - if (request.hasReleaseDate() && !film.getReleaseDate().equals(request.getReleaseDate())) { - updatedFields.add("дата релиза"); - } - if (request.hasDuration() && !film.getDuration().minus(request.getDuration()).isZero()) { - updatedFields.add("продолжительность"); - } - if (request.hasMpa() && !film.getMpaId().equals(request.getMpa().getId())) { - updatedFields.add("рейтинг MPAA"); - } - if (request.hasGenres() && !request.getGenres().stream() - .map(Genre::getId).allMatch(film.getGenreIds()::contains) - ) { - updatedFields.add("жанры"); - } - - String updatedData = String.join(", ", updatedFields); - log.info("Данные для обновления: [{}]", updatedData); - } - - -} diff --git a/src/main/java/ru/yandex/practicum/filmorate/servise/util/LikeAction.java b/src/main/java/ru/yandex/practicum/filmorate/servise/util/LikeAction.java deleted file mode 100644 index 1a4e372..0000000 --- a/src/main/java/ru/yandex/practicum/filmorate/servise/util/LikeAction.java +++ /dev/null @@ -1,5 +0,0 @@ -package ru.yandex.practicum.filmorate.servise.util; - -public enum LikeAction { - SET, REMOVE -} diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 0fb557a..ac774a1 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -5,7 +5,7 @@ CREATE TABLE users ( user_name varchar(50), login varchar(50) NOT NULL, email varchar(50) NOT NULL, - birthday date NOT NULL + birthday date NOT NULL ); CREATE TABLE friends ( @@ -34,6 +34,17 @@ CREATE TABLE genres_of_films ( film_id bigint NOT NULL ); +CREATE TABLE directors ( + id int NOT NULL AUTO_INCREMENT PRIMARY KEY, + director_name varchar(70) NOT NULL +); + +CREATE TABLE directors_of_films ( + id bigint NOT NULL AUTO_INCREMENT PRIMARY KEY, + director_id int NOT NULL, + film_id bigint NOT NULL +); + CREATE TABLE mpa ( id int NOT NULL AUTO_INCREMENT PRIMARY KEY, mpa_name varchar(5) NOT NULL @@ -45,26 +56,72 @@ CREATE TABLE likes ( film_id bigint NOT NULL ); +CREATE TABLE reviews ( + id bigint NOT NULL AUTO_INCREMENT PRIMARY KEY, + content varchar(2048) NOT NULL, + is_positive boolean NOT NULL, + user_id bigint NOT NULL, + film_id bigint NOT NULL, + useful bigint NOT NULL DEFAULT 0 +); + +CREATE TABLE review_likes ( + review_id bigint NOT NULL, + user_id bigint NOT NULL, + is_like boolean NOT NULL, + PRIMARY KEY (review_id, user_id) +); + +CREATE TABLE events ( + id bigint NOT NULL AUTO_INCREMENT PRIMARY KEY, + user_id bigint NOT NULL, + created_at TIMESTAMP NOT NULL, + event_type varchar(20), + operation varchar(20), + entity_id bigint NOT NULL +); + ALTER TABLE friends ADD CONSTRAINT fk_friends_user_id FOREIGN KEY(user_id) -REFERENCES users (id); +REFERENCES users (id) ON DELETE CASCADE; ALTER TABLE friends ADD CONSTRAINT fk_friends_friend_id FOREIGN KEY(friend_id) -REFERENCES users (id); +REFERENCES users (id) ON DELETE CASCADE; ALTER TABLE films ADD CONSTRAINT fk_films_mpa_id FOREIGN KEY(mpa_id) -REFERENCES mpa (id); +REFERENCES mpa (id) ON DELETE SET NULL; ALTER TABLE genres_of_films ADD CONSTRAINT fk_genres_of_films_genre_id FOREIGN KEY(genre_id) -REFERENCES genres (id); +REFERENCES genres (id) ON DELETE CASCADE; ALTER TABLE genres_of_films ADD CONSTRAINT fk_genres_of_films_film_id FOREIGN KEY(film_id) -REFERENCES films (id); +REFERENCES films (id) ON DELETE CASCADE; ALTER TABLE likes ADD CONSTRAINT fk_likes_user_id FOREIGN KEY(user_id) -REFERENCES users (id); +REFERENCES users (id) ON DELETE CASCADE; ALTER TABLE likes ADD CONSTRAINT fk_likes_film_id FOREIGN KEY(film_id) -REFERENCES films (id); +REFERENCES films (id) ON DELETE CASCADE; + +ALTER TABLE DIRECTORS_OF_FILMS ADD CONSTRAINT fk_directors_of_films_director_id FOREIGN KEY(director_id) +REFERENCES directors (id) ON DELETE CASCADE; + +ALTER TABLE DIRECTORS_OF_FILMS ADD CONSTRAINT fk_directors_of_films_film_id FOREIGN KEY(film_id) +REFERENCES films (id) ON DELETE CASCADE; + +ALTER TABLE reviews ADD CONSTRAINT fk_reviews_user_id FOREIGN KEY (user_id) +REFERENCES users (id) ON DELETE CASCADE; + +ALTER TABLE reviews ADD CONSTRAINT fk_reviews_film_id FOREIGN KEY (film_id) +REFERENCES films (id) ON DELETE CASCADE; + +ALTER TABLE review_likes ADD CONSTRAINT fk_review_likes_review_id FOREIGN KEY (review_id) +REFERENCES reviews (id) ON DELETE CASCADE; + +ALTER TABLE review_likes ADD CONSTRAINT fk_review_likes_user_id FOREIGN KEY (user_id) +REFERENCES users (id) ON DELETE CASCADE; + +ALTER TABLE events ADD CONSTRAINT fk_events_user_id FOREIGN KEY(user_id) +REFERENCES users (id) ON DELETE CASCADE; CREATE INDEX IF NOT EXISTS idx_users_user_name ON users (user_name); CREATE UNIQUE INDEX IF NOT EXISTS idx_users_email ON users (email); @@ -75,3 +132,11 @@ CREATE INDEX IF NOT EXISTS idx_friends_friend_id ON friends (friend_id); CREATE INDEX IF NOT EXISTS idx_films_film_name ON films (film_name); +CREATE INDEX IF NOT EXISTS idx_reviews_film_id ON reviews (film_id); +CREATE INDEX IF NOT EXISTS idx_reviews_user_id ON reviews (user_id); +CREATE UNIQUE INDEX IF NOT EXISTS idx_directors_name ON directors (director_name); + +CREATE INDEX IF NOT EXISTS idx_review_likes_review_id ON review_likes (review_id); + +CREATE INDEX IF NOT EXISTS idx_events_user_id ON events (user_id); +CREATE INDEX IF NOT EXISTS idx_events_created_at ON events (created_at); \ No newline at end of file