diff --git a/pom.xml b/pom.xml index c0f548b6..dae45abc 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.boot spring-boot-starter-parent - 2.7.14 + 2.7.17 ru.yandex.practicum @@ -54,6 +54,7 @@ com.h2database h2 + 2.2.220 runtime diff --git a/src/main/java/ru/yandex/practicum/filmorate/FilmorateApplication.java b/src/main/java/ru/yandex/practicum/filmorate/FilmorateApplication.java index 79171d80..1c73d055 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/FilmorateApplication.java +++ b/src/main/java/ru/yandex/practicum/filmorate/FilmorateApplication.java @@ -6,6 +6,7 @@ @SpringBootApplication public class FilmorateApplication { + 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 00000000..85e0faf4 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/DirectorController.java @@ -0,0 +1,44 @@ +package ru.yandex.practicum.filmorate.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import ru.yandex.practicum.filmorate.dto.DirectorDto; +import ru.yandex.practicum.filmorate.service.DirectorService; + +import javax.validation.Valid; +import java.util.Collection; + +@RestController +@RequestMapping("/directors") +@RequiredArgsConstructor +public class DirectorController { + + private final DirectorService directorService; + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public DirectorDto addDirector(@Valid @RequestBody DirectorDto directorDto) { + return directorService.addDirector(directorDto); + } + + @GetMapping + public Collection getAllDirectors() { + return directorService.findAll(); + } + + @GetMapping("/{id}") + public DirectorDto getDirectorById(@PathVariable long id) { + return directorService.getDirectorById(id); + } + + @PutMapping + public DirectorDto updateDirector(@Valid @RequestBody DirectorDto updatedDirectorDto) { + return directorService.updateDirector(updatedDirectorDto); + } + + @DeleteMapping("/{id}") + public void removeDirector(@PathVariable long id) { + directorService.removeDirector(id); + } +} 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 e813df7b..9bd5ebea 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java @@ -48,9 +48,27 @@ public FilmDto removeLike(@PathVariable long id, @PathVariable long userId) { return filmService.removeLike(id, userId); } + @GetMapping("/common") + public Collection getCommonFilms( + @RequestParam long userId, + @RequestParam long friendId) { + return filmService.getCommonFilms(userId, friendId); + + } + @GetMapping("/popular") public Collection getMostPopularFilms(@RequestParam(required = false, defaultValue = "10") int count) { return filmService.getMostPopularFilms(count); } + + @DeleteMapping("/{id}") + public void removeFilm(@PathVariable long id) { + filmService.removeFilm(id); + } + + @GetMapping("/director/{directorId}") + public Collection getFilmsFromDirector(@PathVariable long directorId, @RequestParam String sortBy) { + return filmService.getFilmsFromDirector(directorId, sortBy); + } } 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 00000000..c8a31fb5 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/ReviewController.java @@ -0,0 +1,65 @@ +package ru.yandex.practicum.filmorate.controller; + +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.service.ReviewService; + +import javax.validation.Valid; +import java.util.List; + +@RestController +@RequestMapping("/reviews") +@RequiredArgsConstructor +public class ReviewController { + + private final ReviewService reviewService; + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public ReviewDto addReview(@Valid @RequestBody ReviewDto reviewDto) { + return reviewService.addReview(reviewDto); + } + + @PutMapping + public ReviewDto updateReview(@Valid @RequestBody ReviewDto updatedReviewDto) { + return reviewService.updateReview(updatedReviewDto); + } + + @DeleteMapping("/{id}") + public void deleteReview(@PathVariable long id) { + reviewService.deleteReview(id); + } + + @GetMapping("/{id}") + public ReviewDto getReviewById(@PathVariable long id) { + return reviewService.getReviewById(id); + } + + @GetMapping + public List getReviewsByFilmId(@RequestParam(required = false) Long filmId, + @RequestParam(required = false, defaultValue = "10") int count) { + return reviewService.getReviewsByFilmId(filmId, count); + } + + @PutMapping("/{id}/like/{userId}") + public ReviewDto addLikeToReview(@PathVariable long id, @PathVariable long userId) { + return reviewService.addLikeToReview(id, userId); + } + + @PutMapping("/{id}/dislike/{userId}") + public ReviewDto addDislikeToReview(@PathVariable long id, @PathVariable long userId) { + return reviewService.addDislikeToReview(id, userId); + } + + @DeleteMapping("/{id}/like/{userId}") + public ReviewDto deleteLikeFromReview(@PathVariable long id, @PathVariable long userId) { + return reviewService.deleteLikeFromReview(id, userId); + } + + @DeleteMapping("/{id}/dislike/{userId}") + public ReviewDto deleteDislikeFromReview(@PathVariable long id, @PathVariable long userId) { + return reviewService.deleteDislikeFromReview(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 cc57d70b..4998fbdf 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java @@ -4,6 +4,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; +import ru.yandex.practicum.filmorate.dto.FilmDto; import ru.yandex.practicum.filmorate.dto.UserDto; import ru.yandex.practicum.filmorate.service.UserService; @@ -58,4 +59,14 @@ public void removeFriend(@PathVariable long id, @PathVariable long friendId) { userService.removeFriend(id, friendId); } + @DeleteMapping("/{id}") + public void removeUser(@PathVariable long id) { + userService.removeUser(id); + } + + @GetMapping("/{id}/recommendations") + public Collection showRecommendations(@PathVariable long id) { + return userService.showRecommendations(id); + } + } diff --git a/src/main/java/ru/yandex/practicum/filmorate/dao/DirectorStorage.java b/src/main/java/ru/yandex/practicum/filmorate/dao/DirectorStorage.java new file mode 100644 index 00000000..87d2dfd3 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dao/DirectorStorage.java @@ -0,0 +1,6 @@ +package ru.yandex.practicum.filmorate.dao; + +import ru.yandex.practicum.filmorate.model.Director; + +public interface DirectorStorage extends Dao { +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/dao/FilmDirectorStorage.java b/src/main/java/ru/yandex/practicum/filmorate/dao/FilmDirectorStorage.java new file mode 100644 index 00000000..619cd98d --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dao/FilmDirectorStorage.java @@ -0,0 +1,21 @@ +package ru.yandex.practicum.filmorate.dao; + +import ru.yandex.practicum.filmorate.model.Director; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +public interface FilmDirectorStorage { + void add(long filmId, long directorId); + + void batchUpdate(long filmId, Set directors); + + void deleteAllByFilmId(long filmId); + + Map> findDirectorsInIdList(Set filmIds); + + List findFilmsByDirectorId(long directorId); + + List findAllById(long filmId); +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/dao/FilmGenreStorage.java b/src/main/java/ru/yandex/practicum/filmorate/dao/FilmGenreStorage.java index 98f6be09..109292ab 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dao/FilmGenreStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dao/FilmGenreStorage.java @@ -3,13 +3,17 @@ import ru.yandex.practicum.filmorate.model.Genre; import java.util.List; +import java.util.Map; +import java.util.Set; public interface FilmGenreStorage { void add(long filmId, long genreId); List findAllById(long filmId); + Map> findGenresInIdList(Set filmIds); + void deleteAllById(long filmId); - void batchUpdate(long filmId, List genres); + void batchUpdate(long filmId, Set genres); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/dao/FilmLikeStorage.java b/src/main/java/ru/yandex/practicum/filmorate/dao/FilmLikeStorage.java index 2e8202e1..6e489b5c 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dao/FilmLikeStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dao/FilmLikeStorage.java @@ -1,6 +1,7 @@ package ru.yandex.practicum.filmorate.dao; import java.util.Map; +import java.util.Set; public interface FilmLikeStorage { void add(long filmId, long userId); @@ -10,4 +11,8 @@ public interface FilmLikeStorage { Map findAll(); void remove(long filmId, long userId); + + Set findLikedFilmsByUser(long userId); + + Map> getUsersAndFilmLikes(); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/dao/FilmStorage.java b/src/main/java/ru/yandex/practicum/filmorate/dao/FilmStorage.java index 19d28a99..fa71cd33 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dao/FilmStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dao/FilmStorage.java @@ -3,7 +3,14 @@ import ru.yandex.practicum.filmorate.model.Film; import java.util.Collection; +import java.util.Set; public interface FilmStorage extends Dao { Collection findMostLikedFilmsLimitBy(int count); + + Collection findFilmsByIds(Set filmIds); + + Collection findFilmsFromDirectorOrderBy(long directorId, String sortBy); + + Collection findFilmsByIdsOrderByLikes(Set filmIds); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/dao/ReviewLikeStorage.java b/src/main/java/ru/yandex/practicum/filmorate/dao/ReviewLikeStorage.java new file mode 100644 index 00000000..90d6f0a0 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dao/ReviewLikeStorage.java @@ -0,0 +1,7 @@ +package ru.yandex.practicum.filmorate.dao; + +public interface ReviewLikeStorage { + void add(long reviewId, long userId, String type); + + void delete(long reviewId, long userId, String type); +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/dao/ReviewStorage.java b/src/main/java/ru/yandex/practicum/filmorate/dao/ReviewStorage.java new file mode 100644 index 00000000..a220dbfe --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dao/ReviewStorage.java @@ -0,0 +1,15 @@ +package ru.yandex.practicum.filmorate.dao; + +import ru.yandex.practicum.filmorate.model.Review; + +import java.util.List; + +public interface ReviewStorage extends Dao { + List findByFilmIdLimitBy(long filmId, int count); + + List findAllLimitBy(int count); + + void addLikeToReview(long id); + + void addDislikeToReview(long id); +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/dao/impl/DirectorDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/dao/impl/DirectorDbStorage.java new file mode 100644 index 00000000..8429ac06 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dao/impl/DirectorDbStorage.java @@ -0,0 +1,81 @@ +package ru.yandex.practicum.filmorate.dao.impl; + +import lombok.RequiredArgsConstructor; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.jdbc.support.KeyHolder; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.dao.DirectorStorage; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.model.Director; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Collection; +import java.util.Objects; + +@Repository +@RequiredArgsConstructor +public class DirectorDbStorage implements DirectorStorage { + + private final JdbcTemplate jdbcTemplate; + + @Override + public Director add(final Director director) { + final KeyHolder keyHolder = new GeneratedKeyHolder(); + final String sql = "INSERT INTO director (director_name) VALUES (?)"; + jdbcTemplate.update(con -> { + PreparedStatement stmt = con.prepareStatement(sql, new String[]{"id"}); + stmt.setString(1, director.getName()); + return stmt; + }, keyHolder); + + director.setId(Objects.requireNonNull(keyHolder.getKey(), "Не удалось добавить директора.").longValue()); + + return director; + } + + @Override + public void remove(final long id) { + final String sql = "DELETE FROM director WHERE id = ?"; + final int update = jdbcTemplate.update(sql, id); + if (update != 1) { + throw new NotFoundException("Режиссер с id '" + id + "' не найден."); + } + } + + @Override + public void update(final Director director) { + final String sql = "UPDATE director SET director_name = ? WHERE id = ?"; + final int update = jdbcTemplate.update(sql, director.getName(), director.getId()); + if (update != 1) { + throw new NotFoundException("Режиссер с id '" + director.getId() + "' не найден."); + } + } + + @Override + public Collection findAll() { + final String sql = "SELECT * FROM director"; + return jdbcTemplate.query(sql, this::mapToDirector); + } + + @Override + public Director findById(final long id) { + final String sql = "SELECT * FROM director WHERE id = ?"; + try { + return jdbcTemplate.queryForObject(sql, this::mapToDirector, id); + } catch (EmptyResultDataAccessException e) { + throw new NotFoundException("Режиссер с id '" + id + "' не найден."); + } + } + + private Director mapToDirector(ResultSet rs, int rowNum) throws SQLException { + return Director.builder() + .id(rs.getLong("id")) + .name(rs.getString("director_name")) + .build(); + } + +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/dao/impl/FilmDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/dao/impl/FilmDbStorage.java index 976544b8..ee35de06 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dao/impl/FilmDbStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dao/impl/FilmDbStorage.java @@ -2,14 +2,17 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.dao.DataAccessException; +import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.support.GeneratedKeyHolder; import org.springframework.jdbc.support.KeyHolder; import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.dao.FilmDirectorStorage; import ru.yandex.practicum.filmorate.dao.FilmGenreStorage; import ru.yandex.practicum.filmorate.dao.FilmStorage; import ru.yandex.practicum.filmorate.exception.NotFoundException; +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; @@ -19,6 +22,9 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.util.*; +import java.util.stream.Collectors; + +import static java.util.function.Function.identity; @Repository @RequiredArgsConstructor @@ -29,6 +35,8 @@ public class FilmDbStorage implements FilmStorage { private final FilmGenreStorage filmGenreStorage; + private final FilmDirectorStorage filmDirectorStorage; + @Override public Film add(final Film film) { final KeyHolder keyHolder = new GeneratedKeyHolder(); @@ -46,6 +54,7 @@ public Film add(final Film film) { film.setId(Objects.requireNonNull(keyHolder.getKey(), "Не удалось добавить фильм.").longValue()); filmGenreStorage.batchUpdate(film.getId(), film.getGenres()); + filmDirectorStorage.batchUpdate(film.getId(), film.getDirectors()); return film; } @@ -53,7 +62,10 @@ public Film add(final Film film) { @Override public void remove(final long id) { final String sql = "DELETE FROM film WHERE id = ?"; - jdbcTemplate.update(sql, id); + int amount = jdbcTemplate.update(sql, id); + if (amount != 1) { + throw new NotFoundException("Фильм с id '" + id + "' не найден."); + } } @Override @@ -66,98 +78,130 @@ public void update(final Film film) { } filmGenreStorage.deleteAllById(film.getId()); - filmGenreStorage.batchUpdate(film.getId(), film.getGenres()); + filmDirectorStorage.deleteAllByFilmId(film.getId()); + filmDirectorStorage.batchUpdate(film.getId(), film.getDirectors()); } @Override public Collection findAll() { final String sql = "SELECT " + - "f.ID, f.TITLE, f.DESCRIPTION, f.RELEASE_DATE, f.DURATION, f.MPA_ID, m.RATING_NAME, fg.GENRE_ID, g.GENRE_NAME, COUNT(fl.USER_ID) AS likes " + + "f.ID, f.TITLE, f.DESCRIPTION, f.RELEASE_DATE, f.DURATION, f.MPA_ID, m.RATING_NAME, COUNT(fl.USER_ID) AS likes " + "FROM " + "FILM f LEFT JOIN MPA m ON f.MPA_ID = m.ID " + - "LEFT JOIN FILM_GENRE fg ON f.ID = fg.FILM_ID " + - "LEFT JOIN GENRE g ON fg.GENRE_ID = g.ID " + "LEFT JOIN film_like fl on f.id = fl.film_id " + - "GROUP BY f.id, m.rating_name, fg.genre_id, g.genre_name"; - - return jdbcTemplate.query(sql, this::extractToFilmList); + "GROUP BY f.id, m.rating_name"; + Collection films = jdbcTemplate.query(sql, this::mapToFilm); + setGenresForFilms(films); + setDirectorsForFilms(films); + return films; } @Override public Film findById(final long filmId) { final String sql = "SELECT " + - "f.ID, f.TITLE, f.DESCRIPTION, f.RELEASE_DATE, f.DURATION, f.MPA_ID, m.RATING_NAME, fg.GENRE_ID, g.GENRE_NAME, COUNT(fl.USER_ID) AS likes " + + "f.ID, f.TITLE, f.DESCRIPTION, f.RELEASE_DATE, f.DURATION, f.MPA_ID, m.RATING_NAME, COUNT(fl.USER_ID) AS likes " + "FROM " + "FILM f LEFT JOIN MPA m ON f.MPA_ID = m.ID " + - "LEFT JOIN FILM_GENRE fg ON f.ID = fg.FILM_ID " + - "LEFT JOIN GENRE g ON fg.GENRE_ID = g.ID " + "LEFT JOIN film_like fl on f.id = fl.film_id " + - "GROUP BY f.id, m.rating_name, fg.genre_id, g.genre_name " + + "GROUP BY f.id, m.rating_name " + "HAVING f.ID = ?"; - final Film film = jdbcTemplate.query(sql, this::extractToFilm, filmId); - - if (film == null) { + try { + final Film film = jdbcTemplate.queryForObject(sql, this::mapToFilm, filmId); + List genres = filmGenreStorage.findAllById(filmId); + List directors = filmDirectorStorage.findAllById(filmId); + film.getGenres().addAll(genres); + film.getDirectors().addAll(directors); + return film; + } catch (EmptyResultDataAccessException e) { throw new NotFoundException("Фильм с id '" + filmId + "' не найден."); } - return film; } public Collection findMostLikedFilmsLimitBy(final int count) { final String sql = "SELECT " + - "f.ID, f.TITLE, f.DESCRIPTION, f.RELEASE_DATE, f.DURATION, f.MPA_ID, m.RATING_NAME, fg.GENRE_ID, g.GENRE_NAME, COUNT(fl.USER_ID) AS likes " + + "f.ID, f.TITLE, f.DESCRIPTION, f.RELEASE_DATE, f.DURATION, f.MPA_ID, m.RATING_NAME, COUNT(fl.USER_ID) AS likes " + "FROM " + "FILM f LEFT JOIN MPA m ON f.MPA_ID = m.ID " + - "LEFT JOIN FILM_GENRE fg ON f.ID = fg.FILM_ID " + - "LEFT JOIN GENRE g ON fg.GENRE_ID = g.ID " + "LEFT JOIN film_like fl on f.id = fl.film_id " + - "GROUP BY f.id, m.rating_name, fg.genre_id, g.genre_name " + + "GROUP BY f.id, m.rating_name " + "ORDER BY COUNT(fl.USER_ID) DESC " + "LIMIT ?"; - return jdbcTemplate.query(sql, this::extractToFilmList, count); - } - - private Film extractToFilm(ResultSet rs) throws SQLException, DataAccessException { + Collection films = jdbcTemplate.query(sql, this::mapToFilm, count); + setGenresForFilms(films); + setDirectorsForFilms(films); - Film film = null; - final Map filmIdMap = new HashMap<>(); + return films; + } - while (rs.next()) { + @Override + public Collection findFilmsFromDirectorOrderBy(final long directorId, final String sortBy) { + final List filmsByDirectorId = filmDirectorStorage.findFilmsByDirectorId(directorId); + final String ids = String.join(",", Collections.nCopies(filmsByDirectorId.size(), "?")); + final String sql = String.format( + "SELECT " + + "f.ID, f.TITLE, f.DESCRIPTION, f.RELEASE_DATE, f.DURATION, f.MPA_ID, m.RATING_NAME, COUNT(fl.USER_ID) AS likes " + + "FROM " + + "FILM f LEFT JOIN MPA m ON f.MPA_ID = m.ID " + + "LEFT JOIN film_like fl on f.id = fl.film_id " + + "GROUP BY f.id, m.rating_name " + + "HAVING f.id IN (%s) " + + "ORDER BY ", ids); + final StringBuilder sb = new StringBuilder(); + String sqlWithSort = sb.append(sql).append(sortBy).toString(); + final List directorFilms = jdbcTemplate.query(sqlWithSort, this::mapToFilm, filmsByDirectorId.toArray()); + setGenresForFilms(directorFilms); + setDirectorsForFilms(directorFilms); + + return directorFilms; + } - Long filmId = rs.getLong(1); - film = filmIdMap.get(filmId); - if (film == null) { - film = Film.builder() - .id(filmId) - .name(rs.getString("title")) - .description(rs.getString("description")) - .releaseDate(rs.getDate("release_date").toLocalDate()) - .duration(rs.getInt("duration")) - .mpa(new Mpa(rs.getInt("mpa_id"), rs.getString("rating_name"))) - .build(); - film.setLikes(rs.getLong("likes")); - filmIdMap.put(filmId, film); - } + public Collection findFilmsByIds(Set filmIds) { + final String ids = String.join(",", Collections.nCopies(filmIds.size(), "?")); + final String sql = String.format( + "SELECT " + + "f.ID, f.TITLE, f.DESCRIPTION, f.RELEASE_DATE, f.DURATION, f.MPA_ID, m.RATING_NAME, COUNT(fl.USER_ID) AS likes " + + "FROM " + + "FILM f LEFT JOIN MPA m ON f.MPA_ID = m.ID " + + "LEFT JOIN film_like fl on f.id = fl.film_id " + + "WHERE f.ID IN (%s)" + + "GROUP BY f.id, m.rating_name ", ids); + + Collection films = jdbcTemplate.query(sql, this::mapToFilm, filmIds.toArray()); + return setGenresForFilms(films); + } - final int genre_id = rs.getInt("genre_id"); - if (genre_id == 0) { - film.getGenres().addAll(Collections.emptyList()); - continue; - } + private List setGenresForFilms(Collection films) { + Map filmMap = films.stream().collect(Collectors.toMap(Film::getId, identity())); + Map> filmIdGenreMap = filmGenreStorage.findGenresInIdList(filmMap.keySet()); + filmIdGenreMap.forEach((id, genres) -> filmMap.get(id).getGenres().addAll(genres)); + return new ArrayList<>(filmMap.values()); + } - final Genre genre = new Genre(); - genre.setId(genre_id); - genre.setName(rs.getString("genre_name")); - film.getGenres().add(genre); - } + private List setDirectorsForFilms(Collection films) { + Map filmMap = films.stream().collect(Collectors.toMap(Film::getId, identity())); + Map> filmIdDirectorMap = filmDirectorStorage.findDirectorsInIdList(filmMap.keySet()); + filmIdDirectorMap.forEach((id, directors) -> filmMap.get(id).getDirectors().addAll(directors)); + return new ArrayList<>(filmMap.values()); + } + private Film mapToFilm(ResultSet rs, int rowNum) throws SQLException { + Film film = Film.builder() + .id(rs.getLong(1)) + .name(rs.getString("title")) + .description(rs.getString("description")) + .releaseDate(rs.getDate("release_date").toLocalDate()) + .duration(rs.getInt("duration")) + .mpa(new Mpa(rs.getInt("mpa_id"), rs.getString("rating_name"))) + .build(); + film.setLikes(rs.getLong("likes")); return film; } - private Collection extractToFilmList(ResultSet rs) throws SQLException, DataAccessException { + private Collection extractToFilmList(ResultSet rs) throws SQLException { final Map filmIdMap = new LinkedHashMap<>(); @@ -192,4 +236,39 @@ private Collection extractToFilmList(ResultSet rs) throws SQLException, Da return filmIdMap.values(); } + + @Override + public Collection findFilmsByIdsOrderByLikes(Set filmIds) { + if (filmIds.isEmpty()) { + return Collections.emptyList(); + } + + String placeholders = String.join(",", Collections.nCopies(filmIds.size(), "?")); + + String sql = "SELECT f.ID, f.TITLE, f.DESCRIPTION, f.RELEASE_DATE, f.DURATION, f.MPA_ID, " + + "m.RATING_NAME, COUNT(fl.USER_ID) AS LIKES " + + "FROM FILM f " + + "LEFT JOIN MPA m ON f.MPA_ID = m.ID " + + "LEFT JOIN film_like fl ON f.ID = fl.FILM_ID " + + "WHERE f.ID IN (" + placeholders + ") " + + "GROUP BY f.ID, m.RATING_NAME " + + "ORDER BY LIKES DESC"; + + Object[] idsArray = filmIds.toArray(new Object[0]); + + return jdbcTemplate.query(sql, idsArray, new RowMapper() { + @Override + public Film mapRow(ResultSet rs, int rowNum) throws SQLException { + Film film = new Film(); + film.setId(rs.getLong("ID")); + film.setName(rs.getString("TITLE")); + film.setDescription(rs.getString("DESCRIPTION")); + film.setReleaseDate(rs.getDate("RELEASE_DATE").toLocalDate()); + film.setDuration(rs.getInt("DURATION")); + film.setMpa(new Mpa(rs.getInt("MPA_ID"), rs.getString("RATING_NAME"))); + film.setLikes(rs.getLong("LIKES")); + return film; + } + }); + } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/dao/impl/FilmDirectorDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/dao/impl/FilmDirectorDbStorage.java new file mode 100644 index 00000000..d912872d --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dao/impl/FilmDirectorDbStorage.java @@ -0,0 +1,99 @@ +package ru.yandex.practicum.filmorate.dao.impl; + +import lombok.RequiredArgsConstructor; +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.core.BatchPreparedStatementSetter; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.dao.FilmDirectorStorage; +import ru.yandex.practicum.filmorate.model.Director; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.*; + +@Repository +@RequiredArgsConstructor +public class FilmDirectorDbStorage implements FilmDirectorStorage { + + private final JdbcTemplate jdbcTemplate; + + @Override + public void add(final long filmId, final long directorId) { + final String sql = "INSERT INTO film_director VALUES (?, ?)"; + jdbcTemplate.update(sql, filmId, directorId); + } + + @Override + public void batchUpdate(final long filmId, final Set directors) { + final List directorList = new ArrayList<>(directors); + final String sql = "INSERT INTO film_director (film_id, director_id) VALUES (?, ?)"; + jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() { + @Override + public void setValues(PreparedStatement ps, int i) throws SQLException { + ps.setLong(1, filmId); + ps.setLong(2, directorList.get(i).getId()); + } + + @Override + public int getBatchSize() { + return directors.size(); + } + }); + } + + @Override + public void deleteAllByFilmId(final long filmId) { + final String sql = "DELETE FROM film_director WHERE film_id = ?"; + jdbcTemplate.update(sql, filmId); + } + + @Override + public Map> findDirectorsInIdList(final Set filmIds) { + final String ids = String.join(",", Collections.nCopies(filmIds.size(), "?")); + final String sql = String.format( + "SELECT fd.film_id, fd.director_id, d.director_name FROM film_director fd JOIN director d ON fd.director_id = d.id" + + " WHERE fd.film_id IN (%s)", ids); + + return jdbcTemplate.query(sql, this::extractToMap, filmIds.toArray()); + } + + @Override + public List findFilmsByDirectorId(final long directorId) { + final String sql = "SELECT film_id FROM film_director WHERE director_id = ?"; + return jdbcTemplate.queryForList(sql, Long.class, directorId); + } + + @Override + public List findAllById(long filmId) { + final String sql = "SELECT fd.film_id, fd.director_id, d.director_name FROM film_director fd JOIN director d ON fd.director_id = d.id" + + " WHERE fd.film_id = ?"; + return jdbcTemplate.query(sql, this::mapToDirector, filmId); + } + + private Director mapToDirector(ResultSet rs, int i) throws SQLException { + return Director.builder() + .id(rs.getLong("director_id")) + .name(rs.getString("director_name")) + .build(); + } + + private Map> extractToMap(ResultSet rs) throws SQLException, DataAccessException { + final Map> filmIdDirectorMap = new HashMap<>(); + while (rs.next()) { + final Long filmId = rs.getLong(1); + List directors = filmIdDirectorMap.get(filmId); + if (directors == null) { + directors = new ArrayList<>(); + } + final Director director = Director.builder() + .id(rs.getLong("director_id")) + .name(rs.getString("director_name")) + .build(); + directors.add(director); + filmIdDirectorMap.put(filmId, directors); + } + return filmIdDirectorMap; + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/dao/impl/FilmGenreDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/dao/impl/FilmGenreDbStorage.java index ec9e1b2e..e9c49b0f 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dao/impl/FilmGenreDbStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dao/impl/FilmGenreDbStorage.java @@ -1,6 +1,7 @@ package ru.yandex.practicum.filmorate.dao.impl; import lombok.RequiredArgsConstructor; +import org.springframework.dao.DataAccessException; import org.springframework.jdbc.core.BatchPreparedStatementSetter; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; @@ -10,7 +11,7 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; -import java.util.List; +import java.util.*; @Repository @RequiredArgsConstructor @@ -31,6 +32,16 @@ public List findAllById(final long filmId) { return jdbcTemplate.query(sql, this::mapRowToLong, filmId); } + @Override + public Map> findGenresInIdList(Set filmIds) { + final String ids = String.join(",", Collections.nCopies(filmIds.size(), "?")); + final String sql = String.format( + "SELECT fg.film_id, fg.genre_id, g.genre_name FROM film_genre fg JOIN genre g ON fg.genre_id = g.id" + + " WHERE fg.film_id IN (%s)", ids); + + return jdbcTemplate.query(sql, this::extractToMap, filmIds.toArray()); + } + @Override public void deleteAllById(final long filmId) { final String sql = "DELETE FROM film_genre WHERE film_id = ?"; @@ -38,13 +49,14 @@ public void deleteAllById(final long filmId) { } @Override - public void batchUpdate(final long filmId, final List genres) { - final String sql = "MERGE INTO film_genre (film_id, genre_id) VALUES (?, ?)"; + public void batchUpdate(final long filmId, final Set genres) { + final List genreList = new ArrayList<>(genres); + final String sql = "INSERT INTO film_genre (film_id, genre_id) VALUES (?, ?)"; jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() { @Override public void setValues(PreparedStatement ps, int i) throws SQLException { ps.setLong(1, filmId); - ps.setLong(2, genres.get(i).getId()); + ps.setLong(2, genreList.get(i).getId()); } @Override @@ -57,4 +69,19 @@ public int getBatchSize() { private Genre mapRowToLong(ResultSet rs, int rowNum) throws SQLException { return new Genre(rs.getInt("genre_id"), rs.getString("genre_name")); } + + private Map> extractToMap(ResultSet rs) throws SQLException, DataAccessException { + final Map> filmIdGenreMap = new HashMap<>(); + while (rs.next()) { + final Long filmId = rs.getLong(1); + List genres = filmIdGenreMap.get(filmId); + if (genres == null) { + genres = new ArrayList<>(); + } + final Genre genre = new Genre(rs.getInt("genre_id"), rs.getString("genre_name")); + genres.add(genre); + filmIdGenreMap.put(filmId, genres); + } + return filmIdGenreMap; + } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/dao/impl/FilmLikeDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/dao/impl/FilmLikeDbStorage.java index 07d0a770..13434ab1 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dao/impl/FilmLikeDbStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dao/impl/FilmLikeDbStorage.java @@ -2,6 +2,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.dao.DataAccessException; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; @@ -9,8 +10,7 @@ import java.sql.ResultSet; import java.sql.SQLException; -import java.util.LinkedHashMap; -import java.util.Map; +import java.util.*; @Repository @RequiredArgsConstructor @@ -49,6 +49,19 @@ public void remove(long filmId, long userId) { jdbcTemplate.update(sql, filmId, userId); } + @Override + public Set findLikedFilmsByUser(long userId) { + final String sql = "SELECT film_id FROM film_like WHERE user_id = ?"; + List filmIds = jdbcTemplate.queryForList(sql, Long.class, userId); + return new HashSet<>(filmIds); + } + + @Override + public Map> getUsersAndFilmLikes() { + String filmsIdsSql = "SELECT user_id, film_id FROM film_like"; + return jdbcTemplate.query(filmsIdsSql, this::extractToMap); + } + private Map mapRowToIdCount(ResultSet rs) throws SQLException { final Map result = new LinkedHashMap<>(); while (rs.next()) { @@ -56,4 +69,18 @@ private Map mapRowToIdCount(ResultSet rs) throws SQLException { } return result; } + + private Map> extractToMap(ResultSet rs) throws SQLException, DataAccessException { + final Map> userFilmLikesMap = new HashMap<>(); + while (rs.next()) { + final Long userId = rs.getLong("user_id"); + Set filmLikes = userFilmLikesMap.get(userId); + if (filmLikes == null) { + filmLikes = new HashSet<>(); + } + filmLikes.add(rs.getLong("film_id")); + userFilmLikesMap.put(userId, filmLikes); + } + return userFilmLikesMap; + } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/dao/impl/ReviewDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/dao/impl/ReviewDbStorage.java new file mode 100644 index 00000000..f57f25a8 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dao/impl/ReviewDbStorage.java @@ -0,0 +1,119 @@ +package ru.yandex.practicum.filmorate.dao.impl; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.jdbc.support.KeyHolder; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.dao.ReviewStorage; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.model.Review; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Collection; +import java.util.List; +import java.util.Objects; + +@Repository +@RequiredArgsConstructor +@Slf4j +public class ReviewDbStorage implements ReviewStorage { + + private final JdbcTemplate jdbcTemplate; + + @Override + public Review add(final Review review) { + final KeyHolder keyHolder = new GeneratedKeyHolder(); + final String sql = "INSERT INTO review (review_content, is_positive, useful, user_id, film_id) VALUES (?, ?, ?, ?, ?)"; + jdbcTemplate.update(con -> { + PreparedStatement stmt = con.prepareStatement(sql, new String[]{"id"}); + stmt.setString(1, review.getContent()); + stmt.setBoolean(2, review.isPositive()); + stmt.setLong(3, review.getUseful()); + stmt.setLong(4, review.getUserId()); + stmt.setLong(5, review.getFilmId()); + return stmt; + }, keyHolder); + + review.setReviewId(Objects.requireNonNull(keyHolder.getKey(), "Не удалось добавить отзыв.").longValue()); + + return review; + } + + @Override + public void remove(final long id) { + final String sql = "DELETE FROM review WHERE id = ?"; + int update = jdbcTemplate.update(sql, id); + if (update != 1) { + throw new NotFoundException("Отзыв с id '" + id + "' не найден."); + } + } + + @Override + public void update(final Review review) { + final String sql = "UPDATE review SET review_content = ?, is_positive = ? WHERE id = ?"; + final int update = jdbcTemplate.update(sql, review.getContent(), review.isPositive(), review.getReviewId()); + if (update != 1) { + throw new NotFoundException("Отзыв с id '" + review.getReviewId() + "' не найден."); + } + } + + @Override + public Collection findAll() { + final String sql = "SELECT id, review_content, is_positive, useful, user_id, film_id " + + "FROM review ORDER BY useful DESC, id"; + return jdbcTemplate.query(sql, this::mapReview); + } + + @Override + public Review findById(final long id) { + final String sql = "SELECT id, review_content, is_positive, useful, user_id, film_id " + + "FROM review WHERE id = ?"; + try { + return jdbcTemplate.queryForObject(sql, this::mapReview, id); + } catch (EmptyResultDataAccessException e) { + throw new NotFoundException("Отзыв с id '" + id + "' не найден."); + } + } + + @Override + public List findByFilmIdLimitBy(final long filmId, final int count) { + final String sql = "SELECT id, review_content, is_positive, useful, user_id, film_id " + + "FROM review WHERE film_id = ? ORDER BY useful DESC, id LIMIT ?"; + return jdbcTemplate.query(sql, this::mapReview, filmId, count); + } + + @Override + public List findAllLimitBy(final int count) { + final String sql = "SELECT id, review_content, is_positive, useful, user_id, film_id " + + "FROM review ORDER BY useful DESC, id LIMIT ?"; + return jdbcTemplate.query(sql, this::mapReview, count); + } + + @Override + public void addLikeToReview(final long id) { + final String sql = "UPDATE review SET useful = useful + 1 WHERE id = ?"; + jdbcTemplate.update(sql, id); + } + + @Override + public void addDislikeToReview(long id) { + final String sql = "UPDATE review SET useful = useful - 1 WHERE id = ?"; + jdbcTemplate.update(sql, id); + } + + private Review mapReview(final ResultSet rs, final int rowNum) throws SQLException { + return Review.builder() + .reviewId(rs.getLong("id")) + .content(rs.getString("review_content")) + .isPositive(rs.getBoolean("is_positive")) + .useful(rs.getLong("useful")) + .userId(rs.getLong("user_id")) + .filmId(rs.getLong("film_id")) + .build(); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/dao/impl/ReviewLikeDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/dao/impl/ReviewLikeDbStorage.java new file mode 100644 index 00000000..26cd5a6f --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dao/impl/ReviewLikeDbStorage.java @@ -0,0 +1,27 @@ +package ru.yandex.practicum.filmorate.dao.impl; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.dao.ReviewLikeStorage; + +@Repository +@RequiredArgsConstructor +@Slf4j +public class ReviewLikeDbStorage implements ReviewLikeStorage { + + private final JdbcTemplate jdbcTemplate; + + @Override + public void add(final long reviewId, final long userId, final String type) { + final String sql = "INSERT INTO review_like VALUES (?, ?, ?)"; + jdbcTemplate.update(sql, reviewId, userId, type); + } + + @Override + public void delete(final long reviewId, final long userId, final String type) { + final String sql = "DELETE FROM review_like WHERE review_id = ? AND user_id = ? AND like_type = ?"; + jdbcTemplate.update(sql, userId, type); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/dao/impl/UserDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/dao/impl/UserDbStorage.java index a9b9d9ef..16d3fe20 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dao/impl/UserDbStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dao/impl/UserDbStorage.java @@ -109,7 +109,6 @@ public Collection findCommonFriends(final long userId, final long anotherU return jdbcTemplate.query(sql, this::extractToUserList, userId, anotherUserId); } - private User extractToUser(ResultSet rs) throws SQLException, DataAccessException { User user = null; 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 00000000..6062a71c --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/DirectorDto.java @@ -0,0 +1,19 @@ +package ru.yandex.practicum.filmorate.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class DirectorDto { + + long id; + @NotBlank(message = "Имя режиссера не может быть пустым.") + String name; +} 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 d0be3ce7..72262ce2 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dto/FilmDto.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/FilmDto.java @@ -5,6 +5,7 @@ import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import ru.yandex.practicum.filmorate.model.Director; import ru.yandex.practicum.filmorate.model.Genre; import ru.yandex.practicum.filmorate.model.Mpa; import ru.yandex.practicum.filmorate.validation.PastDate; @@ -13,8 +14,7 @@ import javax.validation.constraints.Positive; import javax.validation.constraints.Size; import java.time.LocalDate; -import java.util.ArrayList; -import java.util.List; +import java.util.LinkedHashSet; @Data @AllArgsConstructor @@ -31,6 +31,7 @@ public class FilmDto { @Positive(message = "Продолжительность должна быть больше нуля") private int duration; //продолжительность фильма private Mpa mpa; //возрастной рейтинг - private final List genres = new ArrayList<>(); //жанры + private final LinkedHashSet genres = new LinkedHashSet<>(); //жанры + private final LinkedHashSet directors = new LinkedHashSet<>(); //режиссеры private long likes; //количество лайков от пользователей } 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 00000000..200676ed --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/ReviewDto.java @@ -0,0 +1,26 @@ +package ru.yandex.practicum.filmorate.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class ReviewDto { + private long reviewId; + @NotBlank(message = "Содержание отзыва не может быть пустым.") + private String content; + @NotNull(message = "Не указана полезность отзыва.") + private Boolean isPositive; + private long useful; + @NotNull(message = "Не указан идентификатор пользователя.") + private Long userId; + @NotNull(message = "Не указан идентификатор фильма.") + private Long filmId; +} 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 00000000..693e4a76 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/mapper/DirectorMapper.java @@ -0,0 +1,23 @@ +package ru.yandex.practicum.filmorate.mapper; + +import lombok.experimental.UtilityClass; +import ru.yandex.practicum.filmorate.dto.DirectorDto; +import ru.yandex.practicum.filmorate.model.Director; + +@UtilityClass +public class DirectorMapper { + + public static DirectorDto toDto(Director director) { + return DirectorDto.builder() + .id(director.getId()) + .name(director.getName()) + .build(); + } + + public static Director toModel(DirectorDto directorDto) { + return Director.builder() + .id(directorDto.getId()) + .name(directorDto.getName()) + .build(); + } +} 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 d39d56c6..be8a94f5 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/mapper/FilmMapper.java +++ b/src/main/java/ru/yandex/practicum/filmorate/mapper/FilmMapper.java @@ -18,6 +18,7 @@ public static FilmDto toDto(Film film) { .likes(film.getLikes()) .build(); filmDto.getGenres().addAll(film.getGenres()); + filmDto.getDirectors().addAll(film.getDirectors()); return filmDto; } @@ -32,6 +33,7 @@ public static Film toModel(FilmDto filmDto) { .likes(filmDto.getLikes()) .build(); film.getGenres().addAll(filmDto.getGenres()); + film.getDirectors().addAll(filmDto.getDirectors()); 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 00000000..d61c233f --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/mapper/ReviewMapper.java @@ -0,0 +1,31 @@ +package ru.yandex.practicum.filmorate.mapper; + +import lombok.experimental.UtilityClass; +import ru.yandex.practicum.filmorate.dto.ReviewDto; +import ru.yandex.practicum.filmorate.model.Review; + +@UtilityClass +public class ReviewMapper { + + public static ReviewDto toDto(Review review) { + return ReviewDto.builder() + .reviewId(review.getReviewId()) + .content(review.getContent()) + .isPositive(review.isPositive()) + .useful(review.getUseful()) + .filmId(review.getFilmId()) + .userId(review.getUserId()) + .build(); + } + + public static Review toModel(ReviewDto reviewDto) { + return Review.builder() + .reviewId(reviewDto.getReviewId()) + .content(reviewDto.getContent()) + .isPositive(reviewDto.getIsPositive()) + .useful(reviewDto.getUseful()) + .filmId(reviewDto.getFilmId()) + .userId(reviewDto.getUserId()) + .build(); + } +} 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 00000000..589d535d --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Director.java @@ -0,0 +1,16 @@ +package ru.yandex.practicum.filmorate.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class Director { + + long id; + String name; +} 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 84932083..f5358805 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/Film.java +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Film.java @@ -7,8 +7,7 @@ import lombok.NoArgsConstructor; import java.time.LocalDate; -import java.util.ArrayList; -import java.util.List; +import java.util.LinkedHashSet; @Data @AllArgsConstructor @@ -21,6 +20,7 @@ public class Film { private LocalDate releaseDate; //дата релиза private int duration; //продолжительность фильма private Mpa mpa; //возрастной рейтинг - private final List genres = new ArrayList<>(); //жанры + private final LinkedHashSet genres = new LinkedHashSet<>(); //жанры + private final LinkedHashSet directors = new LinkedHashSet<>(); //режиссеры private long likes; //количество лайков от пользователей } 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 00000000..9c0977d5 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Review.java @@ -0,0 +1,19 @@ +package ru.yandex.practicum.filmorate.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class Review { + private long reviewId; + private String content; + private boolean isPositive; + private long useful; + private long userId; + private long filmId; +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/ReviewLike.java b/src/main/java/ru/yandex/practicum/filmorate/model/ReviewLike.java new file mode 100644 index 00000000..814fd757 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/ReviewLike.java @@ -0,0 +1,6 @@ +package ru.yandex.practicum.filmorate.model; + +public enum ReviewLike { + LIKE, + DISLIKE; +} 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 00000000..7bdb7976 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/DirectorService.java @@ -0,0 +1,17 @@ +package ru.yandex.practicum.filmorate.service; + +import ru.yandex.practicum.filmorate.dto.DirectorDto; + +import java.util.Collection; + +public interface DirectorService { + DirectorDto addDirector(DirectorDto directorDto); + + Collection findAll(); + + DirectorDto getDirectorById(long id); + + DirectorDto updateDirector(DirectorDto updatedDirectorDto); + + void removeDirector(long id); +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java b/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java index c4724356..6289735b 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java @@ -17,5 +17,11 @@ public interface FilmService { FilmDto removeLike(long filmId, long userId); + void removeFilm(long filmId); + Collection getMostPopularFilms(final int count); + + Collection getCommonFilms(long userId, long friendId); + + Collection getFilmsFromDirector(long directorId, String sortBy); } \ No newline at end of file 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 00000000..71f1acc7 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/ReviewService.java @@ -0,0 +1,26 @@ +package ru.yandex.practicum.filmorate.service; + + +import ru.yandex.practicum.filmorate.dto.ReviewDto; + +import java.util.List; + +public interface ReviewService { + ReviewDto addReview(ReviewDto reviewDto); + + ReviewDto getReviewById(long id); + + ReviewDto updateReview(ReviewDto updatedReviewDto); + + void deleteReview(long id); + + List getReviewsByFilmId(Long filmId, int count); + + ReviewDto addLikeToReview(long id, long userId); + + ReviewDto addDislikeToReview(long id, long userId); + + ReviewDto deleteLikeFromReview(long id, long userId); + + ReviewDto deleteDislikeFromReview(long id, long userId); +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java b/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java index 3c172db2..09fb4a72 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java @@ -1,5 +1,6 @@ package ru.yandex.practicum.filmorate.service; +import ru.yandex.practicum.filmorate.dto.FilmDto; import ru.yandex.practicum.filmorate.dto.UserDto; import java.util.Collection; @@ -21,4 +22,8 @@ public interface UserService { Collection findCommonFriends(long userId, long otherUserId); void removeFriend(long userId, long friendId); + + void removeUser(long userId); + + Collection showRecommendations(long id); } \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/impl/DirectorServiceImpl.java b/src/main/java/ru/yandex/practicum/filmorate/service/impl/DirectorServiceImpl.java new file mode 100644 index 00000000..76a15472 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/impl/DirectorServiceImpl.java @@ -0,0 +1,91 @@ +package ru.yandex.practicum.filmorate.service.impl; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.yandex.practicum.filmorate.dao.DirectorStorage; +import ru.yandex.practicum.filmorate.dto.DirectorDto; +import ru.yandex.practicum.filmorate.mapper.DirectorMapper; +import ru.yandex.practicum.filmorate.model.Director; +import ru.yandex.practicum.filmorate.service.DirectorService; + +import java.util.Collection; +import java.util.stream.Collectors; + +import static ru.yandex.practicum.filmorate.mapper.DirectorMapper.toDto; +import static ru.yandex.practicum.filmorate.mapper.DirectorMapper.toModel; + +@Service +@RequiredArgsConstructor +@Slf4j +public class DirectorServiceImpl implements DirectorService { + + private final DirectorStorage directorStorage; + + /** + * Добавление режиссера в БД. + * + * @param directorDto режиссер. + * @return режиссер с присвоенным идентификатором. + */ + @Transactional + @Override + public DirectorDto addDirector(final DirectorDto directorDto) { + final Director director = toModel(directorDto); + final Director addedDirector = directorStorage.add(director); + log.info("Добавление нового режиссера: {}.", addedDirector); + return toDto(directorStorage.findById(addedDirector.getId())); + } + + /** + * Получение списка всех режиссеров. + * + * @return список всех, хранящихся в БД. + */ + @Override + public Collection findAll() { + log.info("Получение списка всех режиссеров."); + return directorStorage.findAll().stream().map(DirectorMapper::toDto).collect(Collectors.toList()); + } + + /** + * Получение режиссера по идентификатору. + * + * @param id идентфикатор режиссера. + * @return найденный режиссер. + */ + @Override + public DirectorDto getDirectorById(final long id) { + final Director storedDirector = directorStorage.findById(id); + log.info("Режиисер с id '{}' найден: {}.", id, storedDirector); + return toDto(storedDirector); + } + + /** + * Обновление данных режиссера. + * + * @param updatedDirectorDto режиссер с новыми данными, которые необходимо обновить. + * @return обновленный режиссер. + */ + @Transactional + @Override + public DirectorDto updateDirector(final DirectorDto updatedDirectorDto) { + final Director updatedDirector = toModel(updatedDirectorDto); + long directorId = updatedDirector.getId(); + directorStorage.update(updatedDirector); + log.info("Обновление режиссера с id '{}': {}.", directorId, updatedDirector); + return toDto(directorStorage.findById(directorId)); + } + + /** + * Удаление режиссера из БД. + * + * @param id идентификатор режиссера. + */ + @Override + public void removeDirector(final long id) { + directorStorage.remove(id); + log.info("Удаление режиссера с id '{}'", id); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/impl/FilmServiceImpl.java b/src/main/java/ru/yandex/practicum/filmorate/service/impl/FilmServiceImpl.java index 028192ee..9c392831 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/impl/FilmServiceImpl.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/impl/FilmServiceImpl.java @@ -3,6 +3,8 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.yandex.practicum.filmorate.dao.DirectorStorage; import ru.yandex.practicum.filmorate.dao.FilmLikeStorage; import ru.yandex.practicum.filmorate.dao.FilmStorage; import ru.yandex.practicum.filmorate.dao.UserStorage; @@ -11,8 +13,7 @@ import ru.yandex.practicum.filmorate.model.Film; import ru.yandex.practicum.filmorate.service.FilmService; -import java.util.ArrayList; -import java.util.Collection; +import java.util.*; import java.util.stream.Collectors; import static ru.yandex.practicum.filmorate.mapper.FilmMapper.toDto; @@ -23,12 +24,19 @@ @Slf4j public class FilmServiceImpl implements FilmService { + public static final Map ALLOWED_SORTS = Map.of( + "year", "f.release_date", + "likes", "likes DESC" + ); + private final FilmStorage filmStorage; private final UserStorage userStorage; private final FilmLikeStorage filmLikeStorage; + private final DirectorStorage directorStorage; + /** * Добавление фильма в БД. * @@ -36,6 +44,7 @@ public class FilmServiceImpl implements FilmService { * @return фильм с присвоенным идентификатором. */ @Override + @Transactional public FilmDto addFilm(final FilmDto filmDto) { final Film film = toModel(filmDto); final Film addedFilm = filmStorage.add(film); @@ -50,6 +59,7 @@ public FilmDto addFilm(final FilmDto filmDto) { * @return обновленный фильм. */ @Override + @Transactional public FilmDto updateFilm(final FilmDto updatedFilmDto) { final Film updatedFilm = toModel(updatedFilmDto); final long filmId = updatedFilmDto.getId(); @@ -76,6 +86,7 @@ public Collection getAllFilms() { * @return найденный фильм. */ @Override + @Transactional public FilmDto getFilmById(final long filmId) { filmStorage.findById(filmId); log.info("Фильм с id {} найден.", filmId); @@ -85,11 +96,12 @@ public FilmDto getFilmById(final long filmId) { /** * Постановка лайка фильму от пользователя. * - * @param filmId идентификатор фильма, которму ставится лайк. + * @param filmId идентификатор фильма, которому ставится лайк. * @param userId идентификатор пользователя, который ставит лайк. * @return фильм, которому поставили лайк. */ @Override + @Transactional public FilmDto likeFilm(final long filmId, final long userId) { filmStorage.findById(filmId); userStorage.findById(userId); @@ -102,10 +114,11 @@ public FilmDto likeFilm(final long filmId, final long userId) { * Удаление лайка у фильма. * * @param filmId идентификатор фильма, у которого требуется удалить лайк. - * @param userId идентфикатор пользователя лайк которого требуется удалить. + * @param userId идентификатор пользователя лайк которого требуется удалить. * @return фильм, у которого удалили лайк. */ @Override + @Transactional public FilmDto removeLike(final long filmId, final long userId) { filmStorage.findById(filmId); userStorage.findById(userId); @@ -114,6 +127,16 @@ public FilmDto removeLike(final long filmId, final long userId) { return toDto(filmStorage.findById(filmId)); } + /** + * Удаление фильма. + * + * @param filmId идентификатор фильма, который будет удален + */ + @Override + public void removeFilm(long filmId) { + filmStorage.remove(filmId); + } + /** * Получение списка самых популярных фильмов. Под популярностью понимается количество лайков у фильма. Чем больше * лайков, тем популярнее фильм. @@ -125,4 +148,50 @@ public FilmDto removeLike(final long filmId, final long userId) { public Collection getMostPopularFilms(final int count) { return filmStorage.findMostLikedFilmsLimitBy(count).stream().map(FilmMapper::toDto).collect(Collectors.toList()); } + + /** + * Получение списка общих понравившихся фильмов между двумя пользователями. + * Этот метод идентифицирует фильмы, которые были отмечены как понравившиеся обоим пользователям, + * и возвращает их список, отсортированный по убыванию количества лайков + * + * @param userId идентификатор первого пользователя. + * @param friendId идентификатор второго пользователя + * @return список DTO фильмов, которые лайкнуты обоими пользователями. . + * Если общих лайкнутых фильмов нет, возвращается пустой список. + */ + @Override + public Collection getCommonFilms(long userId, long friendId) { + Set userLikedFilmIds = filmLikeStorage.findLikedFilmsByUser(userId); + Set friendLikedFilmIds = filmLikeStorage.findLikedFilmsByUser(friendId); + + userLikedFilmIds.retainAll(friendLikedFilmIds); + + if (userLikedFilmIds.isEmpty()) { + return Collections.emptyList(); + } + + Collection commonFilms = filmStorage.findFilmsByIds(userLikedFilmIds); + return commonFilms.stream().map(FilmMapper::toDto).collect(Collectors.toList()); + } + + + + /** + * Получение списка фильмов режиссера, отсортированных по количеству лайков или году выпуска. + * + * @param directorId идентификатор режиссера. + * @param sortBy поле сортировки. + * @return список фильмов режиссера. + */ + @Override + @Transactional + public Collection getFilmsFromDirector(final long directorId, final String sortBy) { + if (!ALLOWED_SORTS.containsKey(sortBy)) { + throw new IllegalArgumentException("Поле сортировки '" + sortBy + "' не поддерживается."); + } + directorStorage.findById(directorId); + return filmStorage.findFilmsFromDirectorOrderBy(directorId, ALLOWED_SORTS.get(sortBy)).stream() + .map(FilmMapper::toDto) + .collect(Collectors.toList()); + } } \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/impl/ReviewServiceImpl.java b/src/main/java/ru/yandex/practicum/filmorate/service/impl/ReviewServiceImpl.java new file mode 100644 index 00000000..f2d08926 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/impl/ReviewServiceImpl.java @@ -0,0 +1,186 @@ +package ru.yandex.practicum.filmorate.service.impl; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.yandex.practicum.filmorate.dao.*; +import ru.yandex.practicum.filmorate.dto.ReviewDto; +import ru.yandex.practicum.filmorate.mapper.ReviewMapper; +import ru.yandex.practicum.filmorate.model.Review; +import ru.yandex.practicum.filmorate.service.ReviewService; + +import java.util.List; +import java.util.stream.Collectors; + +import static ru.yandex.practicum.filmorate.mapper.ReviewMapper.toDto; +import static ru.yandex.practicum.filmorate.mapper.ReviewMapper.toModel; +import static ru.yandex.practicum.filmorate.model.ReviewLike.DISLIKE; +import static ru.yandex.practicum.filmorate.model.ReviewLike.LIKE; + +@Service +@RequiredArgsConstructor +@Slf4j +public class ReviewServiceImpl implements ReviewService { + + private final ReviewStorage reviewStorage; + private final UserStorage userStorage; + private final FilmStorage filmStorage; + private final ReviewLikeStorage reviewLikeStorage; + + /** + * Добавление отзыва в БД. + * + * @param reviewDto отзыв. + * @return отзыв с присвоенным идентификатором. + */ + @Override + public ReviewDto addReview(final ReviewDto reviewDto) { + findUserAndFilmInDb(reviewDto); + final Review review = toModel(reviewDto); + final Review addedReview = reviewStorage.add(review); + log.info("Добавлен новый отзыв: {}.", addedReview); + return toDto(reviewStorage.findById(addedReview.getReviewId())); + } + + /** + * Получение отзыва по идентификатору. + * + * @param id идентификатор отзыва. + * @return найденный отзыв. + */ + @Override + public ReviewDto getReviewById(final long id) { + final Review review = reviewStorage.findById(id); + log.info("Найден отзыв с id '{}'.", id); + return toDto(review); + } + + /** + * Обновление данных отзыва. Происходит обновление только полей content и isPositive. + * + * @param updatedReviewDto отзыв с обновленными полями. + * @return обновленный отзыв. + */ + @Override + public ReviewDto updateReview(final ReviewDto updatedReviewDto) { + findUserAndFilmInDb(updatedReviewDto); + final Review updatedReview = toModel(updatedReviewDto); + reviewStorage.update(updatedReview); + final long reviewId = updatedReview.getReviewId(); + log.info("Обновление отзыва с id '{}': {}", reviewId, updatedReview); + return toDto(reviewStorage.findById(reviewId)); + } + + /** + * Удаление отзыва из БД. + * + * @param id идентификатор отзыва. + */ + @Override + @Transactional + public void deleteReview(final long id) { + reviewStorage.remove(id); + log.info("Отзыв с id '{}' был удален.", id); + } + + /** + * Получение списка отзывов о фильме. Если идентификатор фильма не был передан, то выводится список всех отзывов. + * + * @param filmId идентификатор фильма. + * @param count количество отзывов, которое требуется вывести. По умолчанию 10. + * @return список отзывов. + */ + @Override + @Transactional + public List getReviewsByFilmId(final Long filmId, final int count) { + if (filmId == null) { + final List reviews = reviewStorage.findAllLimitBy(count); + log.info("Запрос на получение отзывов."); + return reviews.stream().map(ReviewMapper::toDto).collect(Collectors.toList()); + } else { + filmStorage.findById(filmId); + final List reviews = reviewStorage.findByFilmIdLimitBy(filmId, count); + log.info("Запрос на получение отзывов по фильму с id '{}'.", filmId); + return reviews.stream().map(ReviewMapper::toDto).collect(Collectors.toList()); + } + } + + /** + * Добавление лайка отзыву. + * + * @param id идентификатор отзыва. + * @param userId идентификатор пользователя, который ставит лайк. + * @return отзыв с добавленным лайком. + */ + @Override + @Transactional + public ReviewDto addLikeToReview(final long id, final long userId) { + findReviewAndUserInDb(id, userId); + reviewStorage.addLikeToReview(id); + reviewLikeStorage.add(id, userId, LIKE.toString()); + log.info("Пользователь с id '{}' поставил лайк отзыву с id '{}'", userId, id); + return toDto(reviewStorage.findById(id)); + } + + /** + * Добавление дизлайка отзыву. + * + * @param id идентификатор отзыва. + * @param userId идентификатор пользователя, который ставит дизлайк. + * @return отзыв с добавленным дизлайком. + */ + @Override + @Transactional + public ReviewDto addDislikeToReview(long id, long userId) { + findReviewAndUserInDb(id, userId); + reviewStorage.addDislikeToReview(id); + reviewLikeStorage.add(id, userId, DISLIKE.toString()); + log.info("Пользователь с id '{}' поставил дизлайк отзыву с id '{}'", userId, id); + return toDto(reviewStorage.findById(id)); + } + + /** + * Удаление лайка у отзыва. + * + * @param id идентификатор отзыва. + * @param userId идентификатор пользователя, который удаляет лайк. + * @return отзыв с удаленным лайком. + */ + @Override + @Transactional + public ReviewDto deleteLikeFromReview(long id, long userId) { + findReviewAndUserInDb(id, userId); + reviewStorage.addDislikeToReview(id); + reviewLikeStorage.delete(id, userId, LIKE.toString()); + log.info("Пользователь с id '{}' удалил лайк отзыву с id '{}'", userId, id); + return toDto(reviewStorage.findById(id)); + } + + /** + * Удаление дизлайка у отзыва. + * + * @param id идентификатор отзыва. + * @param userId идентификатор пользователя, который удаляет дизлайк. + * @return отзыв с удаленным дизлайком. + */ + @Override + @Transactional + public ReviewDto deleteDislikeFromReview(long id, long userId) { + findReviewAndUserInDb(id, userId); + reviewStorage.addLikeToReview(id); + reviewLikeStorage.delete(id, userId, DISLIKE.toString()); + log.info("Пользователь с id '{}' удалил дизлайк отзыву с id '{}'", userId, id); + return toDto(reviewStorage.findById(id)); + } + + private void findReviewAndUserInDb(long id, long userId) { + reviewStorage.findById(id); + userStorage.findById(userId); + } + + private void findUserAndFilmInDb(ReviewDto updatedReviewDto) { + userStorage.findById(updatedReviewDto.getUserId()); + filmStorage.findById(updatedReviewDto.getFilmId()); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/impl/UserServiceImpl.java b/src/main/java/ru/yandex/practicum/filmorate/service/impl/UserServiceImpl.java index c5abaae8..cf91de0e 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/impl/UserServiceImpl.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/impl/UserServiceImpl.java @@ -3,16 +3,20 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.dao.FilmLikeStorage; +import ru.yandex.practicum.filmorate.dao.FilmStorage; import ru.yandex.practicum.filmorate.dao.FriendshipStorage; import ru.yandex.practicum.filmorate.dao.UserStorage; +import ru.yandex.practicum.filmorate.dto.FilmDto; import ru.yandex.practicum.filmorate.dto.UserDto; +import ru.yandex.practicum.filmorate.mapper.FilmMapper; import ru.yandex.practicum.filmorate.mapper.UserMapper; +import ru.yandex.practicum.filmorate.model.Film; import ru.yandex.practicum.filmorate.model.Friendship; import ru.yandex.practicum.filmorate.model.User; import ru.yandex.practicum.filmorate.service.UserService; -import java.util.ArrayList; -import java.util.Collection; +import java.util.*; import java.util.stream.Collectors; import static ru.yandex.practicum.filmorate.model.FriendshipStatus.ACKNOWLEDGED; @@ -24,8 +28,9 @@ public class UserServiceImpl implements UserService { private final UserStorage userStorage; - + private final FilmStorage filmStorage; private final FriendshipStorage friendshipStorage; + private final FilmLikeStorage filmLikeStorage; /** * Сохранение пользователя в БД. @@ -151,6 +156,53 @@ public void removeFriend(final long userId, final long friendId) { log.info("Пользователи с id {} и {} перестали быть друзьями", userId, friendId); } + /** + * Удаление пользователя. + * + * @param userId идентификатор пользователя, который будет удален + */ + @Override + public void removeUser(long userId) { + userStorage.remove(userId); + } + + /** + * Создание рекомендаций пользователю фильмов, которые ему могут понравиться. Для подготовки рекомендаций + * выгружаем данные из таблицы film_like, находим пользователей с максимальным количеством одинаковых с нашим + * пользователем лайков и выбираем у них для рекомендации, которые они тоже залайкали, но наш пользователь + * в запросе их еще не видел + * + * @param id идентификатор пользователя + * @return коллекцию FilmDto + */ + + @Override + public Collection showRecommendations(long id) { + log.info("Получение списка рекомендаций фильмов для пользователя с id {}.", id); + Map> usersLikes = filmLikeStorage.getUsersAndFilmLikes(); + int maxLikes = 0; + Set recommendations = new HashSet<>(); + Set userLikedFilms = usersLikes.get(id); + for (Long user : usersLikes.keySet()) { + if (user != id) { + Set likedFilms = usersLikes.get(user); + Set currentUserLikedFilms = new HashSet<>(userLikedFilms); + currentUserLikedFilms.retainAll(likedFilms); + if (currentUserLikedFilms.size() > maxLikes && currentUserLikedFilms.size() < likedFilms.size()) { + recommendations.clear(); + maxLikes = currentUserLikedFilms.size(); + likedFilms.removeAll(currentUserLikedFilms); + recommendations.addAll(likedFilms); + } else if (currentUserLikedFilms.size() == maxLikes) { + likedFilms.removeAll(currentUserLikedFilms); + recommendations.addAll(likedFilms); + } + } + } + Collection filmsRecommendation = filmStorage.findFilmsByIds(recommendations); + return filmsRecommendation.stream().map(FilmMapper::toDto).collect(Collectors.toList()); + } + private UserDto validateUserName(final UserDto userDto) { final String validatedName = userDto.getName() == null || userDto.getName().isBlank() ? userDto.getLogin() : userDto.getName(); diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index d6a6e230..d861bc52 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -1,4 +1,4 @@ -DROP TABLE IF EXISTS GENRE, MPA, FILM, FILM_GENRE, FILMORATE_USER, FRIENDSHIP_STATUS, FRIENDSHIP, FILM_LIKE; +DROP TABLE IF EXISTS GENRE, MPA, FILM, FILM_GENRE, FILMORATE_USER, FRIENDSHIP_STATUS, FRIENDSHIP, FILM_LIKE, REVIEW, REVIEW_LIKE, DIRECTOR, FILM_DIRECTOR; CREATE TABLE IF NOT EXISTS GENRE( ID INTEGER GENERATED BY DEFAULT AS IDENTITY(START WITH 1) NOT NULL PRIMARY KEY, @@ -22,13 +22,15 @@ CREATE TABLE IF NOT EXISTS FILM( CREATE TABLE IF NOT EXISTS FILM_GENRE( + ID INTEGER GENERATED BY DEFAULT AS IDENTITY(START WITH 1) NOT NULL PRIMARY KEY, FILM_ID INTEGER NOT NULL, GENRE_ID INTEGER NOT NULL, - CONSTRAINT pk_film_genre PRIMARY KEY (FILM_ID, GENRE_ID), - FOREIGN KEY (FILM_ID) REFERENCES FILM(ID), + FOREIGN KEY (FILM_ID) REFERENCES FILM(ID) ON DELETE CASCADE, FOREIGN KEY (GENRE_ID) REFERENCES GENRE(ID) ); +CREATE UNIQUE INDEX FILM_GENRE_IDX ON FILM_GENRE (ID, FILM_ID, GENRE_ID); + CREATE TABLE IF NOT EXISTS FILMORATE_USER( ID INTEGER GENERATED BY DEFAULT AS IDENTITY(START WITH 1) NOT NULL PRIMARY KEY, EMAIL CHARACTER VARYING(255) NOT NULL, @@ -57,6 +59,41 @@ CREATE TABLE IF NOT EXISTS FILM_LIKE( FILM_ID INTEGER NOT NULL, USER_ID INTEGER NOT NULL, CONSTRAINT pk_film_like PRIMARY KEY (FILM_ID, USER_ID), - FOREIGN KEY (FILM_ID) REFERENCES FILM(ID), + FOREIGN KEY (FILM_ID) REFERENCES FILM(ID) ON DELETE CASCADE, FOREIGN KEY (USER_ID) REFERENCES FILMORATE_USER(ID) -); \ No newline at end of file +); + +CREATE TABLE IF NOT EXISTS REVIEW( + ID INTEGER GENERATED BY DEFAULT AS IDENTITY(START WITH 1) NOT NULL PRIMARY KEY, + REVIEW_CONTENT CHARACTER VARYING(255) NOT NULL, + IS_POSITIVE BOOLEAN NOT NULL, + USEFUL INTEGER, + USER_ID INTEGER NOT NULL, + FILM_ID INTEGER NOT NULL, + FOREIGN KEY (USER_ID) REFERENCES FILMORATE_USER(ID) ON DELETE CASCADE, + FOREIGN KEY (FILM_ID) REFERENCES FILM(ID) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS REVIEW_LIKE( + REVIEW_ID INTEGER NOT NULL, + USER_ID INTEGER NOT NULL, + LIKE_TYPE CHARACTER VARYING(7) NOT NULL, + FOREIGN KEY (REVIEW_ID) REFERENCES REVIEW(ID) ON DELETE CASCADE, + FOREIGN KEY (USER_ID) REFERENCES FILMORATE_USER(ID) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS DIRECTOR( + ID INTEGER GENERATED BY DEFAULT AS IDENTITY(START WITH 1) NOT NULL PRIMARY KEY, + DIRECTOR_NAME CHARACTER VARYING(255) NOT NULL +); + +CREATE TABLE IF NOT EXISTS FILM_DIRECTOR( + ID INTEGER GENERATED BY DEFAULT AS IDENTITY(START WITH 1) NOT NULL PRIMARY KEY, + FILM_ID INTEGER NOT NULL, + DIRECTOR_ID INTEGER NOT NULL, + FOREIGN KEY (FILM_ID) REFERENCES FILM(ID) ON DELETE CASCADE, + FOREIGN KEY (DIRECTOR_ID) REFERENCES DIRECTOR(ID) ON DELETE CASCADE +); + +CREATE UNIQUE INDEX FILM_DIRECTOR_IDX ON FILM_DIRECTOR (ID, FILM_ID, DIRECTOR_ID); + diff --git a/src/test/java/ru/yandex/practicum/filmorate/storage/DirectorDbStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/storage/DirectorDbStorageTest.java new file mode 100644 index 00000000..55b76278 --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/DirectorDbStorageTest.java @@ -0,0 +1,139 @@ +package ru.yandex.practicum.filmorate.storage; + +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.jdbc.core.JdbcTemplate; +import ru.yandex.practicum.filmorate.dao.DirectorStorage; +import ru.yandex.practicum.filmorate.dao.impl.DirectorDbStorage; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.model.Director; + +import java.util.Collection; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@JdbcTest +@RequiredArgsConstructor(onConstructor_ = @Autowired) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class DirectorDbStorageTest { + + private final JdbcTemplate jdbcTemplate; + + private DirectorStorage directorStorage; + private Director director1; + private Director director2; + + @BeforeAll + public void init() { + directorStorage = new DirectorDbStorage(jdbcTemplate); + director1 = Director.builder() + .id(1) + .name("Director 1") + .build(); + director2 = Director.builder() + .id(2) + .name("Director 2") + .build(); + } + + @Test + @DisplayName("Получение списка режиссеров при пустой БД.") + public void testFindEmptyDirectors() { + Collection emptyDirectors = directorStorage.findAll(); + + assertThat(emptyDirectors) + .isNotNull() + .isEmpty(); + } + + @Test + @DisplayName("Получение режиссера с несуществующим id.") + public void findDirectorUnknownId() { + + final long unknownId = 1; + NotFoundException e = assertThrows(NotFoundException.class, () -> directorStorage.findById(unknownId)); + + assertEquals("Режиссер с id '" + unknownId + "' не найден.", e.getMessage()); + } + + @Test + @DisplayName("Добавление режиссера и поиск по id.") + public void testAddAndFindById() { + directorStorage.add(director1); + Director storedDirector = directorStorage.findById(director1.getId()); + + assertThat(storedDirector) + .isNotNull() + .usingRecursiveComparison() + .isEqualTo(director1); + } + + @Test + @DisplayName("Добавление второго режиссера и поиск всех режиссеров.") + public void testAddAndFindAll() { + directorStorage.add(director1); + directorStorage.add(director2); + Collection directors = directorStorage.findAll(); + + assertThat(directors) + .isNotNull() + .isNotEmpty() + .usingRecursiveComparison() + .isEqualTo(List.of(director1, director2)); + } + + @Test + @DisplayName("Обновление данных режиссера.") + public void testUpdate() { + directorStorage.add(director1); + + director1.setName("Updated director 1"); + directorStorage.update(director1); + Director updatedDirector = directorStorage.findById(director1.getId()); + + assertThat(updatedDirector) + .isNotNull() + .usingRecursiveComparison() + .isEqualTo(director1); + } + + @Test + @DisplayName("Удаление режиссера с неизвестным id.") + public void testDeleteUnknownDirectorId() { + final long unknownId = 1; + + NotFoundException e = assertThrows(NotFoundException.class, () -> directorStorage.remove(unknownId)); + + assertEquals("Режиссер с id '" + unknownId + "' не найден.", e.getMessage()); + } + + @Test + @DisplayName("Удаление единственного режиссера.") + public void testDeleteOnlyDirector() { + directorStorage.add(director1); + directorStorage.remove(director1.getId()); + NotFoundException e = assertThrows(NotFoundException.class, () -> directorStorage.findById(director1.getId())); + + assertEquals("Режиссер с id '" + director1.getId() + "' не найден.", e.getMessage()); + } + + @Test + @DisplayName("Удаление режиссера из списка.") + public void testDeleteDirector() { + directorStorage.add(director1); + directorStorage.add(director2); + directorStorage.remove(director1.getId()); + Collection directors = directorStorage.findAll(); + + assertThat(directors) + .isNotNull() + .isNotEmpty() + .usingRecursiveComparison() + .isEqualTo(List.of(director2)); + } +} \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/storage/FilmDbStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/storage/FilmDbStorageTest.java index 846692ea..28af267b 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/storage/FilmDbStorageTest.java +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/FilmDbStorageTest.java @@ -8,19 +8,11 @@ import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.annotation.DirtiesContext; -import ru.yandex.practicum.filmorate.dao.FilmGenreStorage; -import ru.yandex.practicum.filmorate.dao.FilmLikeStorage; -import ru.yandex.practicum.filmorate.dao.FilmStorage; -import ru.yandex.practicum.filmorate.dao.UserStorage; -import ru.yandex.practicum.filmorate.dao.impl.FilmDbStorage; -import ru.yandex.practicum.filmorate.dao.impl.FilmGenreDbStorage; -import ru.yandex.practicum.filmorate.dao.impl.FilmLikeDbStorage; -import ru.yandex.practicum.filmorate.dao.impl.UserDbStorage; +import ru.yandex.practicum.filmorate.dao.*; +import ru.yandex.practicum.filmorate.dao.impl.*; import ru.yandex.practicum.filmorate.exception.NotFoundException; -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.model.User; +import ru.yandex.practicum.filmorate.model.*; +import ru.yandex.practicum.filmorate.service.impl.FilmServiceImpl; import java.time.LocalDate; import java.util.Collection; @@ -39,17 +31,22 @@ public class FilmDbStorageTest { private FilmStorage filmDbStorage; private FilmGenreStorage filmGenreStorage; private FilmLikeStorage filmLikeStorage; + private DirectorStorage directorStorage; private UserStorage userStorage; private Film film; + private Film film2; private Film updatedFilm; private User user; + private Director director; @BeforeEach public void setUp() { filmLikeStorage = new FilmLikeDbStorage(jdbcTemplate); filmGenreStorage = new FilmGenreDbStorage(jdbcTemplate); - filmDbStorage = new FilmDbStorage(jdbcTemplate, filmGenreStorage); + directorStorage = new DirectorDbStorage(jdbcTemplate); + FilmDirectorStorage filmDirectorStorage = new FilmDirectorDbStorage(jdbcTemplate); + filmDbStorage = new FilmDbStorage(jdbcTemplate, filmGenreStorage, filmDirectorStorage); userStorage = new UserDbStorage(jdbcTemplate); Mpa mpa = new Mpa(1, "G"); @@ -63,6 +60,15 @@ public void setUp() { .mpa(mpa) .build(); + film2 = Film.builder() + .id(2) + .name("film") + .description("film description") + .releaseDate(LocalDate.of(2019, 12, 12)) + .duration(123) + .mpa(mpa) + .build(); + updatedFilm = Film.builder() .id(1) .name("updated film") @@ -73,6 +79,11 @@ public void setUp() { .build(); user = new User(1, "email", "login", "name", LocalDate.now()); + + director = Director.builder() + .id(1) + .name("Director") + .build(); } @Test @@ -286,4 +297,106 @@ void testFindByIdWithAllFields() { .usingRecursiveComparison() .isEqualTo(film); } + + @Test + @DisplayName("Тест удаление фильма") + void testDeleteFilm() { + Film newFilm = filmDbStorage.add(film); + filmDbStorage.remove(newFilm.getId()); + + String formattedResponse = String.format("Фильм с id '%s' не найден.", newFilm.getId()); + NotFoundException e = assertThrows(NotFoundException.class, () -> filmDbStorage.findById(newFilm.getId())); + assertEquals(formattedResponse, e.getMessage()); + } + + @Test + @DisplayName("Тест удаление несуществующего фильма") + void testDeleteNotExistingUser() { + int filmId = 999; + String formattedResponse = String.format("Фильм с id '%s' не найден.", filmId); + NotFoundException e = assertThrows(NotFoundException.class, () -> filmDbStorage.remove(filmId)); + assertEquals(formattedResponse, e.getMessage()); + } + + @Test + @DisplayName("Тест на получение самых популярных фильмов с несколькими жанрами") + void testMostPopularFilmsWithSeveralGenres() { + Genre genre1 = new Genre(1, "Комедия"); + Genre genre2 = new Genre(6, "Боевик"); + Genre genre3 = new Genre(2, "Драма"); + + userStorage.add(user); + film.getGenres().add(genre1); + film.getGenres().add(genre2); + film.getGenres().add(genre3); + filmDbStorage.add(film); + filmLikeStorage.add(film.getId(), user.getId()); + filmDbStorage.add(updatedFilm); + + film.setLikes(1); + + Collection popularFilms = filmDbStorage.findMostLikedFilmsLimitBy(1); + + assertThat(popularFilms) + .isNotNull() + .isNotEmpty() + .isEqualTo(List.of(film)); + } + + @Test + @DisplayName("Тест получения фильмов режиссера c сортировкой по году.") + public void findFilmsByDirectorSortByYear() { + film.getDirectors().add(director); + film2.getDirectors().add(director); + directorStorage.add(director); + filmDbStorage.add(film); + filmDbStorage.add(film2); + + Collection films = filmDbStorage.findFilmsFromDirectorOrderBy(director.getId(), + FilmServiceImpl.ALLOWED_SORTS.get("year")); + + assertThat(films) + .isNotNull() + .isNotEmpty() + .usingRecursiveComparison() + .isEqualTo(List.of(film2, film)); + } + + @Test + @DisplayName("Тест получения фильмов режиссера c сортировкой по лайкам.") + public void findFilmsByDirectorSortByLikes() { + film.getDirectors().add(director); + film2.getDirectors().add(director); + film.setLikes(1); + + userStorage.add(user); + directorStorage.add(director); + filmDbStorage.add(film); + filmDbStorage.add(film2); + filmLikeStorage.add(1, 1); + System.out.println(film.getId()); + System.out.println(film2.getId()); + + + Collection films = filmDbStorage.findFilmsFromDirectorOrderBy(director.getId(), + FilmServiceImpl.ALLOWED_SORTS.get("likes")); + + assertThat(films) + .isNotNull() + .isNotEmpty() + .usingRecursiveComparison() + .isEqualTo(List.of(film, film2)); + } + + @Test + @DisplayName("Тест получения пустого списка, когда у режиссера нет фильмов.") + public void findFilmsByDirectorUnknownId() { + directorStorage.add(director); + Collection films = filmDbStorage.findFilmsFromDirectorOrderBy(director.getId(), + FilmServiceImpl.ALLOWED_SORTS.get("year")); + + assertThat(films) + .isNotNull() + .isEmpty(); + } } diff --git a/src/test/java/ru/yandex/practicum/filmorate/storage/ReviewDbStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/storage/ReviewDbStorageTest.java new file mode 100644 index 00000000..252166eb --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/ReviewDbStorageTest.java @@ -0,0 +1,300 @@ +package ru.yandex.practicum.filmorate.storage; + +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.annotation.DirtiesContext; +import ru.yandex.practicum.filmorate.dao.*; +import ru.yandex.practicum.filmorate.dao.impl.*; +import ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.model.Mpa; +import ru.yandex.practicum.filmorate.model.Review; +import ru.yandex.practicum.filmorate.model.User; + +import java.time.LocalDate; +import java.util.Collection; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@JdbcTest +@RequiredArgsConstructor(onConstructor_ = @Autowired) +@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) +class ReviewDbStorageTest { + + private final JdbcTemplate jdbcTemplate; + + private ReviewStorage reviewStorage; + private FilmStorage filmStorage; + private UserStorage userStorage; + private Review review1; + private Review review2; + private Review review3; + private Review updatedReview; + private Film film; + private User user; + + @BeforeEach + public void setUp() { + reviewStorage = new ReviewDbStorage(jdbcTemplate); + FilmGenreStorage filmGenreStorage = new FilmGenreDbStorage(jdbcTemplate); + FilmDirectorStorage filmDirectorStorage = new FilmDirectorDbStorage(jdbcTemplate); + filmStorage = new FilmDbStorage(jdbcTemplate, filmGenreStorage, filmDirectorStorage); + userStorage = new UserDbStorage(jdbcTemplate); + Mpa mpa = new Mpa(1, "G"); + + film = Film.builder() + .id(1) + .name("film") + .description("film description") + .releaseDate(LocalDate.of(2020, 12, 12)) + .duration(123) + .mpa(mpa) + .build(); + + user = User.builder() + .id(1) + .email("email") + .login("login") + .name("name") + .birthday(LocalDate.now()) + .build(); + filmStorage.add(film); + userStorage.add(user); + + + review1 = Review.builder() + .reviewId(1) + .content("review 2") + .isPositive(true) + .useful(1) + .userId(1) + .filmId(1) + .build(); + + review2 = Review.builder() + .reviewId(2) + .content("review 1") + .isPositive(false) + .useful(2) + .userId(1) + .filmId(1) + .build(); + + review3 = Review.builder() + .reviewId(3) + .content("review 3") + .isPositive(true) + .useful(3) + .userId(1) + .filmId(1) + .build(); + + updatedReview = Review.builder() + .reviewId(1) + .content("updated review 1") + .isPositive(true) + .useful(13) + .userId(4) + .filmId(4) + .build(); + } + + @Test + @DisplayName("Тест добавления отзыва и получения по id.") + public void addAndGetByIdTest() { + reviewStorage.add(review1); + + Review savedReview = reviewStorage.findById(1); + + assertThat(savedReview) + .isNotNull() + .usingRecursiveComparison() + .isEqualTo(review1); + } + + @Test + @DisplayName("Тест получения всех отзывов (сортировка по полезности).") + public void getAllReviews() { + reviewStorage.add(review1); + reviewStorage.add(review2); + reviewStorage.add(review3); + Collection reviews = reviewStorage.findAll(); + + assertThat(reviews) + .isNotNull() + .isNotEmpty() + .usingRecursiveComparison() + .isEqualTo(List.of(review3, review2, review1)); + } + + @Test + @DisplayName("Тест получения всех отзывов без оценок полезности.") + public void getAllReviewsWithZeroUseful() { + review1.setUseful(0); + review2.setUseful(0); + review3.setUseful(0); + reviewStorage.add(review1); + reviewStorage.add(review2); + reviewStorage.add(review3); + Collection reviews = reviewStorage.findAll(); + + assertThat(reviews) + .isNotNull() + .isNotEmpty() + .usingRecursiveComparison() + .isEqualTo(List.of(review1, review2, review3)); + } + + @Test + @DisplayName("Тест получения всех отзывов при пустой базе данных.") + public void getAllReviewsEmptyDb() { + Collection reviews = reviewStorage.findAll(); + + assertThat(reviews) + .isNotNull() + .isEmpty(); + } + + @Test + @DisplayName("Тест получения отзывов с ограничением по количеству.") + public void getAllReviewsLimitBy() { + reviewStorage.add(review1); + reviewStorage.add(review2); + reviewStorage.add(review3); + Collection reviews = reviewStorage.findAllLimitBy(2); + + assertThat(reviews) + .isNotNull() + .isNotEmpty() + .usingRecursiveComparison() + .isEqualTo(List.of(review3, review2)); + } + + @Test + @DisplayName("Тест получения отзывов с ограничением по количеству.") + public void getAllReviewsLimitBy10() { + reviewStorage.add(review1); + reviewStorage.add(review2); + reviewStorage.add(review3); + Collection reviews = reviewStorage.findAllLimitBy(10); + + assertThat(reviews) + .isNotNull() + .isNotEmpty() + .usingRecursiveComparison() + .isEqualTo(List.of(review3, review2, review1)); + } + + + @Test + @DisplayName("Тест получения отзывов по фильму с ограничением по количеству.") + public void getAllReviewsFromFilmLimitBy() { + reviewStorage.add(review1); + reviewStorage.add(review2); + reviewStorage.add(review3); + Collection reviews = reviewStorage.findByFilmIdLimitBy(1, 1); + + assertThat(reviews) + .isNotNull() + .isNotEmpty() + .usingRecursiveComparison() + .isEqualTo(List.of(review3)); + } + + @Test + @DisplayName("Тест получения отзывов по фильму с ограничением по количеству.") + public void getAllReviewsFromFilmLimitBy10() { + reviewStorage.add(review1); + reviewStorage.add(review2); + reviewStorage.add(review3); + Collection reviews = reviewStorage.findByFilmIdLimitBy(1, 10); + + assertThat(reviews) + .isNotNull() + .isNotEmpty() + .usingRecursiveComparison() + .isEqualTo(List.of(review3, review2, review1)); + } + + @Test + @DisplayName("Тест удаления отзыва.") + public void deleteReview() { + reviewStorage.add(review1); + reviewStorage.remove(1); + Collection reviews = reviewStorage.findAll(); + + assertThat(reviews) + .isNotNull() + .isEmpty(); + } + + @Test + @DisplayName("Тест обновления отзыва. Обновиться должны только поля content, isPositive.") + public void updateReview() { + reviewStorage.add(review1); + reviewStorage.update(updatedReview); + + Review storedReview = reviewStorage.findById(1); + + assertThat(storedReview) + .isNotNull() + .usingRecursiveComparison() + .comparingOnlyFields("content", "isPositive") + .isEqualTo(updatedReview); + + assertThat(storedReview) + .isNotNull() + .usingRecursiveComparison() + .comparingOnlyFields("useful", "filmId", "userId") + .isEqualTo(review1); + } + + @Test + @DisplayName("Тест добавления лайка отзыву.") + public void addLikeToReview() { + reviewStorage.add(review1); + + reviewStorage.addLikeToReview(1); + Review storedReview = reviewStorage.findById(1); + assertEquals(2, storedReview.getUseful()); + } + + @Test + @DisplayName("Тест добавления лайка отзыву c отрицательным рейтингом.") + public void addLikeToReviewNegativeUseful() { + review1.setUseful(-1); + reviewStorage.add(review1); + + reviewStorage.addLikeToReview(1); + Review storedReview = reviewStorage.findById(1); + assertEquals(0, storedReview.getUseful()); + } + + @Test + @DisplayName("Тест добавления дизлайка отзыву.") + public void addDislikeToReview() { + reviewStorage.add(review1); + + reviewStorage.addDislikeToReview(1); + Review storedReview = reviewStorage.findById(1); + assertEquals(0, storedReview.getUseful()); + } + + @Test + @DisplayName("Тест добавления дизлайка отзыву c отрицательным рейтингом.") + public void addDislikeToReviewNegativeUseful() { + review1.setUseful(-1); + reviewStorage.add(review1); + + reviewStorage.addDislikeToReview(1); + Review storedReview = reviewStorage.findById(1); + assertEquals(-2, storedReview.getUseful()); + } + +} \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/storage/UserDbStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/storage/UserDbStorageTest.java index 5b506415..ef656507 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/storage/UserDbStorageTest.java +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/UserDbStorageTest.java @@ -8,17 +8,17 @@ import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.annotation.DirtiesContext; -import ru.yandex.practicum.filmorate.dao.FriendshipStorage; -import ru.yandex.practicum.filmorate.dao.UserStorage; -import ru.yandex.practicum.filmorate.dao.impl.FriendshipDbStorage; -import ru.yandex.practicum.filmorate.dao.impl.UserDbStorage; +import ru.yandex.practicum.filmorate.dao.*; +import ru.yandex.practicum.filmorate.dao.impl.*; import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.model.Film; import ru.yandex.practicum.filmorate.model.Friendship; +import ru.yandex.practicum.filmorate.model.Mpa; import ru.yandex.practicum.filmorate.model.User; +import ru.yandex.practicum.filmorate.service.impl.UserServiceImpl; import java.time.LocalDate; -import java.util.Collection; -import java.util.List; +import java.util.*; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -34,16 +34,27 @@ class UserDbStorageTest { private final JdbcTemplate jdbcTemplate; private UserStorage userStorage; + private UserServiceImpl userService; private FriendshipStorage friendshipStorage; + private FilmGenreStorage filmGenreStorage; + private FilmLikeStorage filmLikeStorage; + private FilmStorage filmDbStorage; private User user; private User updatedUser; private User anotherUser; + private Film filmOne; + private Film filmTwo; @BeforeEach void setUp() { + filmLikeStorage = new FilmLikeDbStorage(jdbcTemplate); + filmGenreStorage = new FilmGenreDbStorage(jdbcTemplate); + FilmDirectorStorage filmDirectorStorage = new FilmDirectorDbStorage(jdbcTemplate); + filmDbStorage = new FilmDbStorage(jdbcTemplate, filmGenreStorage, filmDirectorStorage); userStorage = new UserDbStorage(jdbcTemplate); friendshipStorage = new FriendshipDbStorage(jdbcTemplate); + userService = new UserServiceImpl(userStorage, filmDbStorage, friendshipStorage, filmLikeStorage); user = User.builder() .id(1) .email("email") @@ -65,6 +76,26 @@ void setUp() { .name("another_name") .birthday(LocalDate.now()) .build(); + + Mpa mpa = new Mpa(1, "G"); + + filmOne = Film.builder() + .id(1) + .name("film") + .description("film description") + .releaseDate(LocalDate.of(2020, 12, 12)) + .duration(123) + .mpa(mpa) + .build(); + + filmTwo = Film.builder() + .id(2) + .name("film two") + .description("film two description") + .releaseDate(LocalDate.of(2020, 12, 12)) + .duration(123) + .mpa(mpa) + .build(); } @Test @@ -314,4 +345,64 @@ void testGetEmptyFriendsList() { .isNotNull() .isEmpty(); } + + @Test + @DisplayName("Тест удаление пользователя") + void testDeleteUser() { + User newUser = userStorage.add(user); + userStorage.remove(newUser.getId()); + + String formattedResponse = String.format("Пользователь с id '%s' не найден.", newUser.getId()); + NotFoundException e = assertThrows(NotFoundException.class, () -> userStorage.findById(newUser.getId())); + assertEquals(formattedResponse, e.getMessage()); + } + + @Test + @DisplayName("Тест удаление несуществующего пользователя") + void testDeleteNotExistingUser() { + int userId = 999; + String formattedResponse = String.format("Пользователь с id '%s' не найден.", userId); + NotFoundException e = assertThrows(NotFoundException.class, () -> userStorage.remove(userId)); + assertEquals(formattedResponse, e.getMessage()); + } + + @Test + @DisplayName("Тест получения мапы с ключами userId и сетом с списком айдишников залайканных фильмов.") + void testGetRecommendationsList() { + userStorage.add(user); + userStorage.add(anotherUser); + + filmDbStorage.add(filmOne); + filmDbStorage.add(filmTwo); + + filmLikeStorage.add(filmOne.getId(), user.getId()); + filmLikeStorage.add(filmOne.getId(), anotherUser.getId()); + filmLikeStorage.add(filmTwo.getId(), anotherUser.getId()); + + Map> filmRecommendations = filmLikeStorage.getUsersAndFilmLikes(); + + assertThat(filmRecommendations.get(1L)) + .isNotNull() + .isNotEmpty() + .containsExactly(filmOne.getId()); + + assertThat(filmRecommendations.get(2L)) + .isNotNull() + .isNotEmpty() + .containsExactly(filmOne.getId(), filmTwo.getId()); + } + + @Test + @DisplayName("Тест получения мапы с ключами userId и сетом с списком айдишников залайканных фильмов, когда лайков нет.") + void testGetRecommendationsListNoLikes() { + userStorage.add(user); + userStorage.add(anotherUser); + filmDbStorage.add(filmOne); + + Map> filmRecommendations = filmLikeStorage.getUsersAndFilmLikes(); + + assertThat(filmRecommendations) + .isNotNull() + .isEmpty(); + } } \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/validation/DirectorValidationTest.java b/src/test/java/ru/yandex/practicum/filmorate/validation/DirectorValidationTest.java new file mode 100644 index 00000000..106fd51b --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/validation/DirectorValidationTest.java @@ -0,0 +1,50 @@ +package ru.yandex.practicum.filmorate.validation; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import ru.yandex.practicum.filmorate.dto.DirectorDto; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static ru.yandex.practicum.filmorate.validation.ValidationTestUtils.VALIDATOR; +import static ru.yandex.practicum.filmorate.validation.ValidationTestUtils.dtoHasErrorMessage; + +public class DirectorValidationTest { + + @ParameterizedTest + @ValueSource(strings = {"", " ", " ", " "}) + @DisplayName("Проверка невозможности добавить режиссера с пустым именем") + public void createDirectorWithoutName(String name) { + DirectorDto directorDto = DirectorDto.builder() + .id(1) + .name(name) + .build(); + + assertTrue(dtoHasErrorMessage(directorDto, "Имя режиссера не может быть пустым.")); + + } + + @Test + @DisplayName("Проверка невозможности добавить режиссера, если name == null") + public void createDirectorWithNullName() { + DirectorDto directorDto = DirectorDto.builder() + .id(1) + .name(null) + .build(); + + assertTrue(dtoHasErrorMessage(directorDto, "Имя режиссера не может быть пустым.")); + } + + @Test + @DisplayName("Проверка добавления режиссера с валидными полями.") + public void createDirector() { + DirectorDto directorDto = DirectorDto.builder() + .id(1) + .name("Director") + .build(); + assertTrue(VALIDATOR.validate(directorDto).isEmpty()); + } +} + + diff --git a/src/test/java/ru/yandex/practicum/filmorate/validation/ReviewValidationTest.java b/src/test/java/ru/yandex/practicum/filmorate/validation/ReviewValidationTest.java new file mode 100644 index 00000000..e97dcfe0 --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/validation/ReviewValidationTest.java @@ -0,0 +1,106 @@ +package ru.yandex.practicum.filmorate.validation; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import ru.yandex.practicum.filmorate.dto.ReviewDto; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static ru.yandex.practicum.filmorate.validation.ValidationTestUtils.VALIDATOR; +import static ru.yandex.practicum.filmorate.validation.ValidationTestUtils.dtoHasErrorMessage; + +public class ReviewValidationTest { + + @Test + @DisplayName("Проверка возможности добавить отзыв с корректными полями") + public void createReview() { + ReviewDto reviewDto = ReviewDto.builder() + .reviewId(1) + .content("content") + .isPositive(true) + .useful(1) + .filmId(1L) + .userId(1L) + .build(); + + assertTrue(VALIDATOR.validate(reviewDto).isEmpty()); + } + + @ParameterizedTest + @ValueSource(strings = {"", " ", " ", " "}) + @DisplayName("Проверка невозможности добавить отзыв с пустым полем content") + public void createReviewWithoutContent(String content) { + ReviewDto reviewDto = ReviewDto.builder() + .reviewId(1) + .content(content) + .isPositive(true) + .useful(1) + .filmId(1L) + .userId(1L) + .build(); + + assertTrue(dtoHasErrorMessage(reviewDto, "Содержание отзыва не может быть пустым.")); + + } + + @Test + @DisplayName("Проверка невозможности добавить отзыв, если content == null") + public void createReviewWithNullContent() { + ReviewDto reviewDto = ReviewDto.builder() + .reviewId(1) + .content(null) + .isPositive(true) + .useful(1) + .filmId(1L) + .userId(1L) + .build(); + + assertTrue(dtoHasErrorMessage(reviewDto, "Содержание отзыва не может быть пустым.")); + } + + @Test + @DisplayName("Проверка невозможности добавить отзыв, если не указан filmId") + public void createReviewWithNullFilmId() { + ReviewDto reviewDto = ReviewDto.builder() + .reviewId(1) + .content("content") + .isPositive(true) + .useful(1) + .filmId(null) + .userId(1L) + .build(); + + assertTrue(dtoHasErrorMessage(reviewDto, "Не указан идентификатор фильма.")); + } + + @Test + @DisplayName("Проверка невозможности добавить отзыв, если не указан userId") + public void createReviewWithNullUserId() { + ReviewDto reviewDto = ReviewDto.builder() + .reviewId(1) + .content("content") + .isPositive(true) + .useful(1) + .filmId(1L) + .userId(null) + .build(); + + assertTrue(dtoHasErrorMessage(reviewDto, "Не указан идентификатор пользователя.")); + } + + @Test + @DisplayName("Проверка невозможности добавить отзыв, если не указана полезность") + public void createReviewWithNullIsPositive() { + ReviewDto reviewDto = ReviewDto.builder() + .reviewId(1) + .content("content") + .isPositive(null) + .useful(1) + .filmId(1L) + .userId(1L) + .build(); + + assertTrue(dtoHasErrorMessage(reviewDto, "Не указана полезность отзыва.")); + } +}