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, "Не указана полезность отзыва."));
+ }
+}