diff --git a/Filmorate.png b/Filmorate.png new file mode 100644 index 0000000..3924819 Binary files /dev/null and b/Filmorate.png differ diff --git a/pom.xml b/pom.xml index 7cd7b4c..2dfade4 100644 --- a/pom.xml +++ b/pom.xml @@ -15,7 +15,9 @@ filmorate 21 + 3.3.1 + org.springframework.boot @@ -49,6 +51,23 @@ logbook-spring-boot-starter 3.7.2 + + + org.springframework.boot + spring-boot-starter-jdbc + + + + org.junit.jupiter + junit-jupiter + test + + + + com.h2database + h2 + runtime + @@ -57,6 +76,28 @@ org.springframework.boot spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-checkstyle-plugin + ${maven-checkstyle-plugin.version} + + + true + true + true + checkstyle.xml + + + + + + check + + compile + + + 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 6f50e3c..792a515 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java @@ -1,67 +1,68 @@ package ru.yandex.practicum.filmorate.controller; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Positive; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import ru.yandex.practicum.filmorate.model.Film; import ru.yandex.practicum.filmorate.service.FilmService; -import ru.yandex.practicum.filmorate.storage.FilmStorage; import java.util.Collection; @Slf4j @RestController +@RequiredArgsConstructor @RequestMapping("/films") public class FilmController { - private final FilmStorage inMemoryFilmStorage; - private final FilmService filmService; - - @Autowired - public FilmController(FilmStorage inMemoryFilmStorage, FilmService filmService) { - this.inMemoryFilmStorage = inMemoryFilmStorage; - this.filmService = filmService; - } + private final FilmService service; @GetMapping public Collection findAll() { log.info("Список фильмов выведен"); - return inMemoryFilmStorage.findAll(); + return service.findAll(); } @GetMapping("/{id}") - public Film findById(@PathVariable("id") int filmId) { - log.info("Фильм с id: {} выведен", filmId); - return inMemoryFilmStorage.findFilmById(filmId); + public Film findById(@PathVariable @Positive int id) { + log.info("Фильм с id: {} выведен", id); + return service.findFilmById(id); } @GetMapping("/popular") - public Collection getPopular(@RequestParam(defaultValue = "10") int count) { + public Collection getPopular(@RequestParam(defaultValue = "10") @Positive int count) { log.info("Список популярных фильмов выведен"); - return filmService.getPopular(count); + return service.getPopular(count); } @PostMapping - public Film create(@RequestBody Film film) { + public Film create(@Valid @RequestBody Film film) { log.info("Фильм: {} добавлен в базу", film); - return inMemoryFilmStorage.create(film); + return service.create(film); } @PutMapping - public Film update(@RequestBody Film newFilm) { + public Film update(@Valid @RequestBody Film newFilm) { log.info("Данные о фильме: {} обновлены", newFilm); - return inMemoryFilmStorage.update(newFilm); + return service.update(newFilm); } @PutMapping("/{id}/like/{userId}") - public void addLike(@PathVariable int id, @PathVariable int userId) { + public Film addLike(@Positive @PathVariable int id, @Positive @PathVariable int userId) { log.info("Фильму с id: {} поставил лайк пользователь с id: {}", id, userId); - filmService.addLike(id, userId); + return service.addLike(id, userId); } @DeleteMapping("/{id}/like/{userId}") - public void deleteLike(@PathVariable int id, @PathVariable int userId) { + public Film deleteLike(@Positive @PathVariable int id, @Positive @PathVariable int userId) { log.info("У фильма с id: {} убрал лайк пользователь с id: {}", id, userId); - filmService.deleteLike(id, userId); + return service.deleteLike(id, userId); + } + + @DeleteMapping("/{id}") + public void remove(@PathVariable @Positive int id) { + log.info("Фильм с id: {} удален", id); + service.remove(id); } } \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/GenreController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/GenreController.java new file mode 100644 index 0000000..fd791cf --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/GenreController.java @@ -0,0 +1,34 @@ +package ru.yandex.practicum.filmorate.controller; + +import jakarta.validation.constraints.Positive; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import ru.yandex.practicum.filmorate.model.Genre; +import ru.yandex.practicum.filmorate.service.GenreService; + +import java.util.Collection; + +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/genres") +public class GenreController { + + private final GenreService service; + + @GetMapping + public Collection findAll() { + log.info("Список жанров выведен"); + return service.findAll(); + } + + @GetMapping("/{id}") + public Genre findGenreById(@Positive @PathVariable("id") int id) { + log.info("Жанр с id: {} выведен", id); + return service.findGenreById(id); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/RatingController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/RatingController.java new file mode 100644 index 0000000..7c3c4b2 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/RatingController.java @@ -0,0 +1,34 @@ +package ru.yandex.practicum.filmorate.controller; + +import jakarta.validation.constraints.Positive; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import ru.yandex.practicum.filmorate.model.Rating; +import ru.yandex.practicum.filmorate.service.RatingService; + +import java.util.Collection; + +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/mpa") +public class RatingController { + + private final RatingService service; + + @GetMapping + public Collection findAll() { + log.info("Список рейтингов выведен"); + return service.findAll(); + } + + @GetMapping("/{id}") + public Rating findRatingById(@Positive @PathVariable("id") int id) { + log.info("Рейтинг с id: {} выведен", id); + return service.findRatingById(id); + } +} 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 c134492..ed484f8 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java @@ -1,71 +1,74 @@ package ru.yandex.practicum.filmorate.controller; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Positive; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.*; import ru.yandex.practicum.filmorate.model.User; import ru.yandex.practicum.filmorate.service.UserService; -import ru.yandex.practicum.filmorate.storage.InMemoryUserStorage; import java.util.Collection; @Slf4j @RestController +@RequiredArgsConstructor @RequestMapping("/users") public class UserController { - private final InMemoryUserStorage inMemoryUserStorage; - private final UserService userService; - - public UserController(InMemoryUserStorage inMemoryUserStorage, UserService userService) { - this.inMemoryUserStorage = inMemoryUserStorage; - this.userService = userService; - } + private final UserService service; @GetMapping public Collection findAll() { log.info("Список пользователей выведен"); - return inMemoryUserStorage.findAll(); + return service.findAll(); } @GetMapping("/{id}") - public User findUserById(@PathVariable("id") int userId) { - log.info("Пользователь с id: {} выведен", userId); - return inMemoryUserStorage.findUserById(userId); + public User findUserById(@Positive @PathVariable int id) { + log.info("Пользователь с id: {} выведен", id); + return service.findUserById(id); } @GetMapping("/{id}/friends") - public Collection findAllFriends(@PathVariable("id") int userId) { + public Collection findAllFriends(@Positive @PathVariable("id") int userId) { log.info("Список друзей пользователя с id: {} выведен", userId); - return userService.getFriendList(userId); + return service.getFriendList(userId); } @GetMapping("/{id}/friends/common/{otherId}") - public Collection findAllCommonFriends(@PathVariable int id, @PathVariable int otherId) { + public Collection findAllCommonFriends(@Positive @PathVariable int id, @Positive @PathVariable int otherId) { log.info("Список общих друзей пользователей с id: {} и {} выведен", id, otherId); - return userService.getCommonFriendList(id, otherId); + return service.getCommonFriendList(id, otherId); } @PostMapping - public User create(@RequestBody User user) { + public User create(@Valid @RequestBody User user) { log.info("Пользоввтель: {} создан и добавлен", user); - return inMemoryUserStorage.create(user); + return service.create(user); } @PutMapping - public User update(@RequestBody User newUser) { + public User update(@Valid @RequestBody User newUser) { log.info("Данные о пользователе: {} обновлены", newUser); - return inMemoryUserStorage.update(newUser); + return service.update(newUser); } @PutMapping("/{id}/friends/{friendId}") public void addFriend(@PathVariable("id") int userId, @PathVariable int friendId) { log.info("Пользователь с id: {} добавил пользователя с id: {} в друзья", userId, friendId); - userService.addFriend(userId, friendId); + service.addFriend(userId, friendId); } @DeleteMapping("/{id}/friends/{friendId}") public void deleteFriend(@PathVariable("id") int userId, @PathVariable int friendId) { log.info("Пользователь с id: {} удалил пользователя с id: {} из друзей", userId, friendId); - userService.deleteFriend(userId, friendId); + service.deleteFriend(userId, friendId); + } + + @DeleteMapping("/{id}") + public void remove(@Positive @PathVariable int id) { + log.info("Пользователь с id: {} удален", id); + service.remove(id); } } \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/dao/BaseDao.java b/src/main/java/ru/yandex/practicum/filmorate/dao/BaseDao.java new file mode 100644 index 0000000..e72f1b2 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dao/BaseDao.java @@ -0,0 +1,69 @@ +package ru.yandex.practicum.filmorate.dao; + +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import ru.yandex.practicum.filmorate.exception.InternalServerException; + +import java.sql.PreparedStatement; +import java.sql.Statement; +import java.util.List; + +public abstract class BaseDao { + protected final JdbcTemplate jdbc; + protected final NamedParameterJdbcTemplate namedJdbc; + protected final RowMapper mapper; + + public BaseDao(JdbcTemplate jdbc, RowMapper mapper) { + this.jdbc = jdbc; + this.namedJdbc = new NamedParameterJdbcTemplate(jdbc); + this.mapper = mapper; + } + + protected T get(String query, Object... params) { + return jdbc.queryForObject(query, mapper, params); + } + + public List getAll(String query, Object... params) { + return jdbc.query(query, mapper, params); + } + + public void delete(String query, Integer id) { + jdbc.update(query, id); + } + + public void update(String query, Object...params) { + int rowsUpdated = jdbc.update(query, params); + if (rowsUpdated == 0) { + try { + throw new InternalServerException("Не удалось обновить данные"); + } catch (InternalServerException e) { + throw new RuntimeException(e); + } + } + } + + public int insert(String query, Object... params) { + GeneratedKeyHolder keyHolder = new GeneratedKeyHolder(); + jdbc.update(connection -> { + PreparedStatement ps = connection + .prepareStatement(query, Statement.RETURN_GENERATED_KEYS); + for (int idx = 0; idx < params.length; idx++) { + ps.setObject(idx + 1, params[idx]); + } + return ps; }, keyHolder); + + Integer id = keyHolder.getKeyAs(Integer.class); + + if (id != null) { + return id; + } else { + try { + throw new InternalServerException("Не удалось сохранить данные"); + } catch (InternalServerException e) { + throw new RuntimeException(e); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/dao/FilmDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/dao/FilmDbStorage.java new file mode 100644 index 0000000..67839e7 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dao/FilmDbStorage.java @@ -0,0 +1,142 @@ +package ru.yandex.practicum.filmorate.dao; + +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.dao.mappers.FilmRowMapper; +import ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.storage.FilmStorage; +import ru.yandex.practicum.filmorate.storage.RatingStorage; + +import java.util.*; + +@Repository("FilmDbStorage") +public class FilmDbStorage extends BaseDao implements FilmStorage { + + private static final String SELECT_ALL_FIELDS = """ + SELECT f.*, + COUNT(DISTINCT l.user_id) AS likes, + r.mpa_id, + r.name AS mpa_name, + ARRAY_AGG(g.genre_id) AS genre_ids, + ARRAY_AGG(g.name) AS genre_names + """; + + private static final String JOIN_ALL_TABLES = """ + FROM films AS f + LEFT JOIN mpa AS r ON f.mpa_id = r.mpa_id + LEFT JOIN likes AS l ON f.film_id = l.film_id + LEFT JOIN films_genres AS fg ON f.film_id = fg.film_id + LEFT JOIN genres AS g ON fg.genre_id = g.genre_id + """; + + private static final String GROUP_BY = "GROUP BY f.film_id"; + + private static final String INSERT_QUERY = """ + INSERT INTO films (name, description, release_date, duration, mpa_id) + VALUES (?, ?, ?, ?, ?) + """; + + private static final String DELETE_QUERY = "DELETE FROM films WHERE film_id = ?"; + + private static final String UPDATE_QUERY = """ + UPDATE films SET + name = ?, + description = ?, + release_date = ?, + duration = ?, + mpa_id = ? + WHERE film_id = ? + """; + + private static final String EXISTS_QUERY = "SELECT EXISTS(SELECT 1 FROM films WHERE film_id = ?)"; + + private static final String FIND_FILM_BY_ID_QUERY = String.format("%s %s WHERE f.film_id = ? %s", + SELECT_ALL_FIELDS, JOIN_ALL_TABLES, GROUP_BY); + + private static final String FIND_ALL_FILMS_QUERY = String.format("%s %s %s", + SELECT_ALL_FIELDS, JOIN_ALL_TABLES, GROUP_BY); + + private final GenreDbStorage genreDbStorage; + private final RatingStorage ratingStorage; + + public FilmDbStorage(JdbcTemplate jdbc, FilmRowMapper mapper, GenreDbStorage genreDbStorage, + RatingStorage ratingStorage) { + super(jdbc, mapper); + this.genreDbStorage = genreDbStorage; + this.ratingStorage = ratingStorage; + } + + @Override + public List findAll() { + return getAll(FIND_ALL_FILMS_QUERY); + } + + @Override + public Film create(Film film) { + + int id = insert(INSERT_QUERY, + film.getName(), + film.getDescription(), + film.getReleaseDate(), + film.getDuration(), + film.getMpa().getId() + ); + film.setId(id); + + return film; + } + + @Override + public Film update(Film film) { + update(UPDATE_QUERY, + film.getName(), + film.getDescription(), + film.getReleaseDate(), + film.getDuration(), + film.getMpa().getId(), + film.getId() + ); + return film; + } + + @Override + public Film findFilmById(int id) { + return get(FIND_FILM_BY_ID_QUERY, id); + + } + + @Override + public Film remove(int id) { + Film film = findFilmById(id); + delete(DELETE_QUERY, id); + return film; + } + + @Override + public List getPopular(int count) { + return jdbc.query(FIND_ALL_FILMS_QUERY + " ORDER BY likes DESC LIMIT ?", mapper, count); + } + + @Override + public Integer addLike(int id, int userId) { + String insert = "INSERT INTO likes (film_id, user_id) VALUES(?, ?)"; + String count = "SELECT COUNT(DISTINCT user_id) FROM likes WHERE film_id = ?"; + jdbc.update(insert, id, userId); + + return jdbc.queryForObject(count, Integer.class, id); + } + + @Override + public Integer deleteLike(int id, int userId) { + String delete = "DELETE FROM likes WHERE user_id = ?"; + String count = "SELECT COUNT(DISTINCT user_id) FROM likes WHERE film_id = ?"; + jdbc.update(delete, userId); + + return jdbc.queryForObject(count, Integer.class, id); + } + + @Override + public boolean contains(Integer id) { + return jdbc.queryForObject(EXISTS_QUERY, Boolean.class, id); + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/dao/GenreDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/dao/GenreDbStorage.java new file mode 100644 index 0000000..a25f54a --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dao/GenreDbStorage.java @@ -0,0 +1,61 @@ +package ru.yandex.practicum.filmorate.dao; + +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.dao.mappers.GenreRowMapper; +import ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.model.Genre; +import ru.yandex.practicum.filmorate.storage.GenreStorage; + +import java.util.List; +import java.util.Optional; + +@Repository("GenreDbStorage") +public class GenreDbStorage extends BaseDao implements GenreStorage { + + private static final String FIND_ALL_QUERY = "SELECT * FROM genres"; + private static final String FIND_BY_ID_QUERY = "SELECT * FROM genres WHERE genre_id = ?"; + private static final String ADD_GENRE_TO_FILM_QUERY = """ + INSERT INTO films_genres (film_id, genre_id) + VALUES (?, ?) + """; + private static final String UPDATE_FILM_GENRE_QUERY = "DELETE FROM films_genres WHERE film_id = ?"; + + public GenreDbStorage(JdbcTemplate jdbc, GenreRowMapper mapper) { + super(jdbc, mapper); + } + + @Override + public List findAll() { + return getAll(FIND_ALL_QUERY); + } + + @Override + public Optional findGenreById(int id) { + try { + Genre genre = get(FIND_BY_ID_QUERY, id); + return Optional.ofNullable(genre); + } catch (EmptyResultDataAccessException e) { + return Optional.empty(); + } + } + + @Override + public void addGenresToFilm(Film film) { + Integer filmId = film.getId(); + + List batchArgs = film.getGenres() + .stream() + .map(genre -> new Object[]{filmId, genre.getId()}) + .toList(); + + jdbc.batchUpdate(ADD_GENRE_TO_FILM_QUERY, batchArgs); + } + + @Override + public void updateFilmGenres(Film film) { + jdbc.update(UPDATE_FILM_GENRE_QUERY, film.getId()); + addGenresToFilm(film); + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/dao/RatingDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/dao/RatingDbStorage.java new file mode 100644 index 0000000..8ffe702 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dao/RatingDbStorage.java @@ -0,0 +1,37 @@ +package ru.yandex.practicum.filmorate.dao; + +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.dao.mappers.RatingRowMapper; +import ru.yandex.practicum.filmorate.model.Rating; +import ru.yandex.practicum.filmorate.storage.RatingStorage; + +import java.util.List; +import java.util.Optional; + +@Repository("RatingDbStorage") +public class RatingDbStorage extends BaseDao implements RatingStorage { + + private static final String FIND_ALL_QUERY = "SELECT * FROM mpa"; + private static final String FIND_BY_ID_QUERY = "SELECT * FROM mpa WHERE mpa_id = ?"; + + public RatingDbStorage(JdbcTemplate jdbc, RatingRowMapper mapper) { + super(jdbc, mapper); + } + + @Override + public List findAll() { + return getAll(FIND_ALL_QUERY); + } + + @Override + public Optional findRatingById(int id) { + try { + Rating rating = get(FIND_BY_ID_QUERY, id); + return Optional.ofNullable(rating); + } catch (EmptyResultDataAccessException e) { + return Optional.empty(); + } + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/dao/UserDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/dao/UserDbStorage.java new file mode 100644 index 0000000..bc6b47b --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dao/UserDbStorage.java @@ -0,0 +1,136 @@ +package ru.yandex.practicum.filmorate.dao; + +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.dao.mappers.UserRowMapper; +import ru.yandex.practicum.filmorate.model.User; +import ru.yandex.practicum.filmorate.storage.UserStorage; + +import java.util.Collections; +import java.util.List; +import java.util.Set; + +@Repository("UserDbStorage") +public class UserDbStorage extends BaseDao implements UserStorage { + + private static final String FIND_ALL_USERS_QUERY = "SELECT * FROM users"; + + private static final String FIND_USER_BY_ID_QUERY = "SELECT * FROM users WHERE user_id = ?"; + + private static final String INSERT_QUERY = """ + INSERT INTO users (name, email, login, birthday) + VALUES (?, ?, ?, ?) + """; + + private static final String UPDATE_QUERY = """ + UPDATE users SET + name = ?, + email = ?, + login = ?, + birthday = ? + WHERE user_id = ? + """; + + private static final String DELETE_QUERY = "DELETE FROM users WHERE user_id = ?"; + + private static final String EXISTS = "SELECT EXISTS(SELECT 1 FROM users WHERE user_id = ?)"; + + private static final String ADD_FRIEND_QUERY = """ + INSERT INTO user_friends (user_id, friend_id) + VALUES (?, ?) + """; + + private static final String DELETE_FRIEND_QUERY = "DELETE FROM user_friends WHERE user_id = ? AND friend_id = ?"; + + private static final String FIND_ALL_FRIENDS_QUERY = "SELECT friend_id FROM user_friends WHERE user_id = ?"; + + private static final String FIND_COMMON_FRIENDS_QUERY = """ + SELECT T1.friend_id + FROM user_friends AS T1 + JOIN user_friends AS T2 ON T1.friend_id = T2.friend_id + WHERE T1.user_id = ? AND T2.user_id = ? + """; + + private static final String SELECT_ALL_FROM_COLLECTION = "SELECT * FROM users WHERE user_id IN (:set)"; + + protected final NamedParameterJdbcTemplate namedJdbc; + + public UserDbStorage(JdbcTemplate jdbc, UserRowMapper mapper, NamedParameterJdbcTemplate namedJdbc) { + super(jdbc, mapper); + + this.namedJdbc = namedJdbc; + } + + @Override + public List findAll() { + return getAll(FIND_ALL_USERS_QUERY); + } + + @Override + public User create(User user) { + int id = insert(INSERT_QUERY, + user.getName(), + user.getEmail(), + user.getLogin(), + user.getBirthday() + ); + user.setId(id); + return user; + } + + @Override + public User update(User user) { + update(UPDATE_QUERY, + user.getName(), + user.getEmail(), + user.getLogin(), + user.getBirthday(), + user.getId() + ); + return user; + } + + @Override + public User findUserById(int id) { + return get(FIND_USER_BY_ID_QUERY, id); + } + + @Override + public void remove(int id) { + delete(DELETE_QUERY, id); + } + + @Override + public void addFriend(int userId, int friendId) { + jdbc.update(ADD_FRIEND_QUERY, userId, friendId); + } + + @Override + public void deleteFriend(int userId, int friendId) { + jdbc.update(DELETE_FRIEND_QUERY, userId, friendId); + } + + @Override + public List getFriendList(int userId) { + Set list = Set.copyOf(jdbc.queryForList(FIND_ALL_FRIENDS_QUERY, Integer.class, userId)); + if (list.isEmpty()) { + return Collections.emptyList(); + } + return namedJdbc.query(SELECT_ALL_FROM_COLLECTION, Collections.singletonMap("set", list), mapper); + } + + @Override + public List getCommonFriendList(int id, int otherId) { + Set list = Set.copyOf(jdbc.queryForList(FIND_COMMON_FRIENDS_QUERY, Integer.class, id, otherId)); + if (list.isEmpty()) { + return Collections.emptyList(); + } + return namedJdbc.query(SELECT_ALL_FROM_COLLECTION, Collections.singletonMap("set", list), mapper); + } + + @Override + public boolean contains(Integer id) { + return jdbc.queryForObject(EXISTS, Boolean.class, id); + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/dao/mappers/FilmRowMapper.java b/src/main/java/ru/yandex/practicum/filmorate/dao/mappers/FilmRowMapper.java new file mode 100644 index 0000000..c6d2f10 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dao/mappers/FilmRowMapper.java @@ -0,0 +1,63 @@ +package ru.yandex.practicum.filmorate.dao.mappers; + +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Component; +import ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.model.Genre; +import ru.yandex.practicum.filmorate.model.Rating; + +import java.sql.Array; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.LocalDate; +import java.util.HashSet; +import java.util.Set; + +@Component +public class FilmRowMapper implements RowMapper { + + @Override + public Film mapRow(ResultSet resultSet, int rowNum) throws SQLException { + Film film = new Film(); + film.setId(resultSet.getInt("film_id")); + film.setName(resultSet.getString("name")); + film.setDescription(resultSet.getString("description")); + film.setDuration(resultSet.getInt("duration")); + LocalDate releaseDate = resultSet.getObject("release_date", LocalDate.class); + film.setReleaseDate(releaseDate); + + mapRating(film, resultSet); + mapGenres(film, resultSet); + return film; + } + + private void mapRating(Film film, ResultSet resultSet) throws SQLException { + int ratingId = resultSet.getInt("mpa_id"); + if (!resultSet.wasNull()) { + String ratingName = resultSet.getString("mpa_name"); + film.setMpa(new Rating(ratingId, ratingName)); + } + } + + private void mapGenres(Film film, ResultSet resultSet) throws SQLException { + Array genresId = resultSet.getArray("genre_ids"); + if (resultSet.wasNull()) { + return; + } + Array genresName = resultSet.getArray("genre_names"); + if (genresName == null) { + return; + } + Object[] ids = (Object[]) genresId.getArray(); + Object[] names = (Object[]) genresName.getArray(); + Set genres = new HashSet<>(); + for (int i = 0; i < ids.length; i++) { + if (ids[i] != null && names[i] != null) { + Integer id = ((Number)ids[i]).intValue(); + String name = names[i].toString(); + genres.add(new Genre(id, name)); + } + } + film.setGenres(genres); + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/dao/mappers/GenreRowMapper.java b/src/main/java/ru/yandex/practicum/filmorate/dao/mappers/GenreRowMapper.java new file mode 100644 index 0000000..1ef7ee0 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dao/mappers/GenreRowMapper.java @@ -0,0 +1,20 @@ +package ru.yandex.practicum.filmorate.dao.mappers; + +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Component; +import ru.yandex.practicum.filmorate.model.Genre; + +import java.sql.ResultSet; +import java.sql.SQLException; + +@Component +public class GenreRowMapper implements RowMapper { + + @Override + public Genre mapRow(ResultSet resultSet, int rowNum) throws SQLException { + Genre genre = new Genre(); + genre.setId(resultSet.getInt("genre_id")); + genre.setName(resultSet.getString("name")); + return genre; + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/dao/mappers/RatingRowMapper.java b/src/main/java/ru/yandex/practicum/filmorate/dao/mappers/RatingRowMapper.java new file mode 100644 index 0000000..eba0af9 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dao/mappers/RatingRowMapper.java @@ -0,0 +1,23 @@ +package ru.yandex.practicum.filmorate.dao.mappers; + +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Component; +import ru.yandex.practicum.filmorate.model.Genre; +import ru.yandex.practicum.filmorate.model.Rating; + +import java.sql.ResultSet; +import java.sql.SQLException; + +@Component +public class RatingRowMapper implements RowMapper { + + Genre genre = new Genre(); + + @Override + public Rating mapRow(ResultSet resultSet, int rowNum) throws SQLException { + Rating rating = new Rating(); + rating.setId(resultSet.getInt("mpa_id")); + rating.setName(resultSet.getString("name")); + return rating; + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/dao/mappers/UserRowMapper.java b/src/main/java/ru/yandex/practicum/filmorate/dao/mappers/UserRowMapper.java new file mode 100644 index 0000000..ea6d290 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dao/mappers/UserRowMapper.java @@ -0,0 +1,25 @@ +package ru.yandex.practicum.filmorate.dao.mappers; + +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Component; +import ru.yandex.practicum.filmorate.model.User; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.LocalDate; + +@Component +public class UserRowMapper implements RowMapper { + + @Override + public User mapRow(ResultSet resultSet, int rowNum) throws SQLException { + User user = new User(); + user.setId(resultSet.getInt("user_id")); + user.setName(resultSet.getString("name")); + user.setLogin(resultSet.getString("login")); + user.setEmail(resultSet.getString("email")); + LocalDate birthday = resultSet.getObject("birthday", LocalDate.class); + user.setBirthday(birthday); + return user; + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/ErrorHandler.java b/src/main/java/ru/yandex/practicum/filmorate/exception/ErrorHandler.java similarity index 54% rename from src/main/java/ru/yandex/practicum/filmorate/controller/ErrorHandler.java rename to src/main/java/ru/yandex/practicum/filmorate/exception/ErrorHandler.java index aaf6f71..b0d770b 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/ErrorHandler.java +++ b/src/main/java/ru/yandex/practicum/filmorate/exception/ErrorHandler.java @@ -1,31 +1,43 @@ -package ru.yandex.practicum.filmorate.controller; +package ru.yandex.practicum.filmorate.exception; +import lombok.extern.slf4j.Slf4j; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.http.HttpStatus; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; -import ru.yandex.practicum.filmorate.exception.NotFoundException; -import ru.yandex.practicum.filmorate.exception.ValidationException; import ru.yandex.practicum.filmorate.model.ErrorResponse; +@Slf4j @RestControllerAdvice public class ErrorHandler { @ExceptionHandler @ResponseStatus(HttpStatus.BAD_REQUEST) - public ErrorResponse handleValidationException(final ValidationException e) { + public ErrorResponse handleValidationExceptions(MethodArgumentNotValidException e) { + log.warn("Ошибка запроса"); return new ErrorResponse(e.getMessage()); } @ExceptionHandler @ResponseStatus(HttpStatus.NOT_FOUND) public ErrorResponse handleNotFoundException(final NotFoundException e) { + log.warn("Объект не найден"); return new ErrorResponse(e.getMessage()); } @ExceptionHandler @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public ErrorResponse handleAllOtherExceptions(Throwable e) { + log.warn("Ошибка сервера"); + return new ErrorResponse(e.getMessage()); + } + + @ExceptionHandler + @ResponseStatus(HttpStatus.NOT_FOUND) + public ErrorResponse handleDataIntegrityViolationException(DataIntegrityViolationException e) { + log.warn("Объект не найден"); return new ErrorResponse(e.getMessage()); } } \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/exception/InternalServerException.java b/src/main/java/ru/yandex/practicum/filmorate/exception/InternalServerException.java new file mode 100644 index 0000000..4ca17a6 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/exception/InternalServerException.java @@ -0,0 +1,7 @@ +package ru.yandex.practicum.filmorate.exception; + +public class InternalServerException extends Throwable { + public InternalServerException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/exception/ReleaseDate.java b/src/main/java/ru/yandex/practicum/filmorate/exception/ReleaseDate.java new file mode 100644 index 0000000..9329055 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/exception/ReleaseDate.java @@ -0,0 +1,18 @@ +package ru.yandex.practicum.filmorate.exception; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = ReleaseDateValidator.class) +public @interface ReleaseDate { + String message() default "Дата выхода не может быть раньше 28 декабря 1895 года."; + Class[] groups() default {}; + Class[] payload() default {}; +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/exception/ReleaseDateValidator.java b/src/main/java/ru/yandex/practicum/filmorate/exception/ReleaseDateValidator.java new file mode 100644 index 0000000..e89e71e --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/exception/ReleaseDateValidator.java @@ -0,0 +1,17 @@ +package ru.yandex.practicum.filmorate.exception; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +import java.time.LocalDate; + +public class ReleaseDateValidator implements ConstraintValidator { + + @Override + public boolean isValid(LocalDate localDate, ConstraintValidatorContext constraintValidatorContext) { + if (localDate == null) { + return true; + } + return !localDate.isBefore(LocalDate.of(1895, 12, 28)); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/ErrorResponse.java b/src/main/java/ru/yandex/practicum/filmorate/model/ErrorResponse.java index 6a9e1f5..9336824 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/ErrorResponse.java +++ b/src/main/java/ru/yandex/practicum/filmorate/model/ErrorResponse.java @@ -1,13 +1,13 @@ package ru.yandex.practicum.filmorate.model; +import lombok.Getter; + +@Getter public class ErrorResponse { - String error; + private final String error; public ErrorResponse(String error) { this.error = error; } - public String getError() { - return error; - } } \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/Film.java b/src/main/java/ru/yandex/practicum/filmorate/model/Film.java index 6fbe6f5..84ab8be 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/Film.java +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Film.java @@ -1,24 +1,28 @@ package ru.yandex.practicum.filmorate.model; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.Size; import lombok.Data; +import ru.yandex.practicum.filmorate.exception.ReleaseDate; + import java.time.LocalDate; import java.util.HashSet; import java.util.Set; -/** - * Film. - */ @Data public class Film { + private Integer id; + @NotBlank(message = "Название не может быть пустым") private String name; + @Size(max = 200, message = "Описание превышает 200 символов") private String description; + @ReleaseDate private LocalDate releaseDate; + @Positive private int duration; - private Set likes = new HashSet<>(); - - public int getRating() { - return likes.size(); - } + private Rating mpa; + private Set genres = new HashSet<>(); } \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/Genre.java b/src/main/java/ru/yandex/practicum/filmorate/model/Genre.java new file mode 100644 index 0000000..df9a9de --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Genre.java @@ -0,0 +1,16 @@ +package ru.yandex.practicum.filmorate.model; + +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class Genre { + + @NotNull + private Integer id; + private String name; +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/Rating.java b/src/main/java/ru/yandex/practicum/filmorate/model/Rating.java new file mode 100644 index 0000000..06c0f8e --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Rating.java @@ -0,0 +1,16 @@ +package ru.yandex.practicum.filmorate.model; + +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class Rating { + + @NotNull + private Integer id; + private String name; +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/User.java b/src/main/java/ru/yandex/practicum/filmorate/model/User.java index e06c2d7..cc96223 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/User.java +++ b/src/main/java/ru/yandex/practicum/filmorate/model/User.java @@ -1,5 +1,8 @@ package ru.yandex.practicum.filmorate.model; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.PastOrPresent; import lombok.Data; import java.time.LocalDate; @@ -9,9 +12,13 @@ @Data public class User { private Integer id; + @NotBlank(message = "Почта не может быть пустой") + @Email(message = "Некорректный формат email") private String email; + @NotBlank(message = "Логин не может быть пустой") private String login; private String name; + @PastOrPresent private LocalDate birthday; private Set friends = new HashSet<>(); } \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java b/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java index 4ea7853..504b1fa 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java @@ -4,55 +4,89 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; -import ru.yandex.practicum.filmorate.exception.ValidationException; +import org.springframework.transaction.annotation.Transactional; +import ru.yandex.practicum.filmorate.exception.NotFoundException; import ru.yandex.practicum.filmorate.model.Film; import ru.yandex.practicum.filmorate.storage.FilmStorage; +import ru.yandex.practicum.filmorate.storage.GenreStorage; import ru.yandex.practicum.filmorate.storage.UserStorage; import java.util.Collection; -import java.util.Comparator; -import java.util.stream.Collectors; @Slf4j @Service +@Transactional public class FilmService { private final FilmStorage filmStorage; private final UserStorage userStorage; + private final GenreStorage genreStorage; @Autowired - public FilmService(@Qualifier("inMemoryFilmStorage") FilmStorage filmStorage, - @Qualifier("inMemoryUserStorage") UserStorage userStorage) { + public FilmService(@Qualifier("FilmDbStorage") FilmStorage filmStorage, + @Qualifier("UserDbStorage") UserStorage userStorage, + @Qualifier("GenreDbStorage") GenreStorage genreStorage) { this.filmStorage = filmStorage; this.userStorage = userStorage; + this.genreStorage = genreStorage; } - public void addLike(int id, int userId) { - Film film = filmStorage.findFilmById(id); - userStorage.findUserById(userId); - if (film.getLikes().contains(userId)) { - log.warn("Ошибка добавления лайка"); - throw new ValidationException("Лайк от этого пользователя уже стоит"); + public Collection findAll() { + return filmStorage.findAll(); + } + + public Film create(Film film) { + film = filmStorage.create(film); + if (film.getGenres() != null && !film.getGenres().isEmpty()) { + genreStorage.addGenresToFilm(film); } - film.getLikes().add(userId); + return film; } - public void deleteLike(int id, int userId) { - Film film = filmStorage.findFilmById(id); - userStorage.findUserById(userId); - if (!film.getLikes().contains(userId)) { - log.warn("Ошибка удаления лайка"); - throw new ValidationException("У фильма нет лайка от этого пользователя"); + public Film update(Film newFilm) { + if (newFilm.getId() == null) { + throw new NotFoundException("Фильм не найден"); } - film.getLikes().remove(userId); + return filmStorage.update(newFilm); } - public Collection getPopular(int count) { - return filmStorage.findAll().stream() - .sorted(comparator) - .limit(count) - .collect(Collectors.toList()); + public Film findFilmById(int id) { + if (!filmStorage.contains(id)) { + throw new NotFoundException("Фильм с id = " + id + " не найден"); + } + return filmStorage.findFilmById(id); } - public static final Comparator comparator = Comparator.comparingInt(Film::getRating).reversed(); + public void remove(int id) { + if (!filmStorage.contains(id)) { + throw new NotFoundException("Фильм с id = " + id + " не найден"); + } + filmStorage.remove(id); + } + + public Film addLike(int id, int userId) { + if (!filmStorage.contains(id)) { + throw new NotFoundException("Фильм с id = " + id + " не найден"); + } + if (!userStorage.contains(userId)) { + throw new NotFoundException("Пользователь с id = " + userId + " не найден"); + } + filmStorage.addLike(id, userId); + return filmStorage.findFilmById(id); + } + + public Film deleteLike(int id, int userId) { + if (!filmStorage.contains(id)) { + throw new NotFoundException("Фильм с id = " + id + " не найден"); + } + if (!userStorage.contains(userId)) { + throw new NotFoundException("Пользователь с id = " + userId + " не найден"); + } + filmStorage.deleteLike(id, userId); + return filmStorage.findFilmById(id); + } + + public Collection getPopular(int count) { + return filmStorage.getPopular(count); + } } \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/GenreService.java b/src/main/java/ru/yandex/practicum/filmorate/service/GenreService.java new file mode 100644 index 0000000..57e4bf8 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/GenreService.java @@ -0,0 +1,27 @@ +package ru.yandex.practicum.filmorate.service; + +import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.model.Genre; +import ru.yandex.practicum.filmorate.storage.GenreStorage; + +import java.util.Collection; + +@Service +public class GenreService { + + private final GenreStorage genreStorage; + + public GenreService(GenreStorage genreStorage) { + this.genreStorage = genreStorage; + } + + public Collection findAll() { + return genreStorage.findAll(); + } + + public Genre findGenreById(int id) { + return genreStorage.findGenreById(id) + .orElseThrow(() -> new NotFoundException("Жанр с id = " + id + " не найден в базе")); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/RatingService.java b/src/main/java/ru/yandex/practicum/filmorate/service/RatingService.java new file mode 100644 index 0000000..7d69999 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/RatingService.java @@ -0,0 +1,27 @@ +package ru.yandex.practicum.filmorate.service; + +import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.model.Rating; +import ru.yandex.practicum.filmorate.storage.RatingStorage; + +import java.util.Collection; + +@Service +public class RatingService { + + private final RatingStorage ratingStorage; + + public RatingService(RatingStorage ratingStorage) { + this.ratingStorage = ratingStorage; + } + + public Collection findAll() { + return ratingStorage.findAll(); + } + + public Rating findRatingById(int id) { + return ratingStorage.findRatingById(id) + .orElseThrow(() -> new NotFoundException("Mpa с id = " + id + " не найден")); + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java b/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java index e03525e..ab4de94 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java @@ -3,37 +3,72 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.yandex.practicum.filmorate.exception.NotFoundException; import ru.yandex.practicum.filmorate.exception.ValidationException; import ru.yandex.practicum.filmorate.model.User; import ru.yandex.practicum.filmorate.storage.UserStorage; -import java.util.ArrayList; import java.util.Collection; -import java.util.List; @Slf4j @Service +@Transactional public class UserService { private final UserStorage userStorage; - public UserService(@Qualifier("inMemoryUserStorage") UserStorage userStorage) { + public UserService(@Qualifier("UserDbStorage") UserStorage userStorage) { this.userStorage = userStorage; } + public Collection findAll() { + return userStorage.findAll(); + } + + public User create(User user) { + if (user.getName() == null || user.getName().isBlank()) { + user.setName(user.getLogin()); + } + user = userStorage.create(user); + return user; + } + + public User update(User newUser) { + if (newUser.getId() == null) { + log.warn("Не указан id"); + throw new ValidationException("Id должен быть указан"); + } + if (!userStorage.contains(newUser.getId())) { + throw new NotFoundException("Пользователь с ID " + newUser.getId() + " не найден"); + } + return userStorage.update(newUser); + } + + public User findUserById(int userId) { + if (!userStorage.contains(userId)) { + log.warn("Пользователь не найден"); + throw new NotFoundException("Пользователя с таким id не найдено"); + } + return userStorage.findUserById(userId); + } + + public void remove(int id) { + if (!userStorage.contains(id)) { + log.warn("Пользователь не найден"); + throw new NotFoundException("Пользователя с таким id не найдено"); + } + userStorage.remove(id); + } + public void addFriend(int userId, int friendId) { - if (userId == friendId) { - log.warn("Ошибка добавления в друзья"); - throw new ValidationException("Пользователь не может добавить сам себя в друзья"); + if (!userStorage.contains(userId)) { + throw new NotFoundException("Пользователь с id = " + userId + " не найден"); } - User user = userStorage.findUserById(userId); - User usersFriend = userStorage.findUserById(friendId); - if (user.getFriends().contains(friendId)) { - log.warn("Ошибка добавления в друзья"); - throw new ValidationException("Пользователь уже добавлен в друзья"); + if (!userStorage.contains(friendId)) { + throw new NotFoundException("Пользователь с id = " + friendId + " не найден"); } - user.getFriends().add(friendId); - usersFriend.getFriends().add(userId); + userStorage.addFriend(userId, friendId); } public void deleteFriend(int userId, int friendId) { @@ -41,30 +76,29 @@ public void deleteFriend(int userId, int friendId) { log.warn("Ошибка удаления из друзей"); throw new ValidationException("Пользователь не может удалить сам себя"); } - User user = userStorage.findUserById(userId); - User usersFriend = userStorage.findUserById(friendId); - user.getFriends().remove(friendId); - usersFriend.getFriends().remove(userId); + if (!userStorage.contains(userId)) { + throw new NotFoundException("Пользователь с id = " + userId + " не найден"); + } + if (!userStorage.contains(friendId)) { + throw new NotFoundException("Пользователь с id = " + friendId + " не найден"); + } + userStorage.deleteFriend(userId, friendId); } public Collection getFriendList(int userId) { - User user = userStorage.findUserById(userId); - List list = new ArrayList<>(); - for (Integer id : user.getFriends()) { - list.add(userStorage.findUserById(id)); + if (!userStorage.contains(userId)) { + throw new NotFoundException("Пользователь с id = " + userId + " не найден"); } - return list; + return userStorage.getFriendList(userId); } public Collection getCommonFriendList(int id, int otherId) { - User user1 = userStorage.findUserById(id); - User user2 = userStorage.findUserById(otherId); - List list = new ArrayList<>(); - for (Integer userid : user1.getFriends()) { - if (user2.getFriends().contains(userid)) { - list.add(userStorage.findUserById(userid)); - } + if (!userStorage.contains(id)) { + throw new NotFoundException("Пользователь с id = " + id + " не найден"); + } + if (!userStorage.contains(otherId)) { + throw new NotFoundException("Пользователь с id = " + otherId + " не найден"); } - return list; + return userStorage.getCommonFriendList(id, otherId); } } \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/FilmStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/FilmStorage.java index 0b380f8..710bbe5 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/FilmStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/FilmStorage.java @@ -13,4 +13,14 @@ public interface FilmStorage { Film update(Film newFilm); Film findFilmById(int id); + + Film remove(int id); + + Collection getPopular(int count); + + Integer addLike(int id, int userId); + + Integer deleteLike(int id, int userId); + + boolean contains(Integer id); } \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/GenreStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/GenreStorage.java new file mode 100644 index 0000000..86d2a4d --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/GenreStorage.java @@ -0,0 +1,18 @@ +package ru.yandex.practicum.filmorate.storage; + +import ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.model.Genre; + +import java.util.Collection; +import java.util.Optional; + +public interface GenreStorage { + + Collection findAll(); + + Optional findGenreById(int id); + + void addGenresToFilm(Film film); + + void updateFilmGenres(Film film); +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/InMemoryFilmStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/InMemoryFilmStorage.java deleted file mode 100644 index b6b340a..0000000 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/InMemoryFilmStorage.java +++ /dev/null @@ -1,87 +0,0 @@ -package ru.yandex.practicum.filmorate.storage; - -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import ru.yandex.practicum.filmorate.exception.ValidationException; -import ru.yandex.practicum.filmorate.exception.NotFoundException; -import ru.yandex.practicum.filmorate.model.Film; - -import java.time.LocalDate; -import java.util.*; - -@Slf4j -@Component("inMemoryFilmStorage") -public class InMemoryFilmStorage implements FilmStorage { - - private final Map films = new HashMap<>(); - - @Override - public Collection findAll() { - return films.values(); - } - - @Override - public Film create(Film film) { - validateFilm(film); - film.setId(getNextId()); - films.put(film.getId(), film); - return film; - } - - @Override - public Film update(Film newFilm) { - if (newFilm.getId() == null) { - log.warn("Не указан id"); - throw new ValidationException("Id должен быть указан"); - } - if (!films.containsKey(newFilm.getId())) { - log.warn("Фильм с указанным id не найден"); - throw new NotFoundException("Фильм с id = " + newFilm.getId() + " не найден"); - } - validateFilm(newFilm); - Film oldFilm = films.get(newFilm.getId()); - oldFilm.setDescription(newFilm.getDescription()); - oldFilm.setDuration(newFilm.getDuration()); - oldFilm.setName(newFilm.getName()); - oldFilm.setReleaseDate(newFilm.getReleaseDate()); - return oldFilm; - } - - @Override - public Film findFilmById(int id) { - if (!films.containsKey(id)) { - throw new NotFoundException("Фильма с таким id не найдено"); - } - return films.get(id); - } - - private int getNextId() { - int currentMaxId = films.keySet() - .stream() - .mapToInt(id -> id) - .max() - .orElse(0); - return ++currentMaxId; - } - - private void validateFilm(Film film) { - if (film.getDescription() != null && film.getDescription().length() > 200) { - log.warn("Ошибка лимита"); - throw new ValidationException("Описание превышает 200 символов"); - } - if (film.getReleaseDate().isBefore(LocalDate.of(1895, 12, 28))) { - log.warn("Ошибка даты"); - throw new ValidationException("Неверная дата релиза"); - } - - if (film.getName() == null || film.getName().isBlank()) { - log.warn("Пустое название фильма"); - throw new ValidationException("Название не может быть пустым"); - } - - if (film.getDuration() < 1) { - log.warn("Ошибка длительности"); - throw new ValidationException("Длительность не может быть меньше 1"); - } - } -} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/InMemoryUserStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/InMemoryUserStorage.java deleted file mode 100644 index d9468af..0000000 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/InMemoryUserStorage.java +++ /dev/null @@ -1,87 +0,0 @@ -package ru.yandex.practicum.filmorate.storage; - -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import ru.yandex.practicum.filmorate.exception.NotFoundException; -import ru.yandex.practicum.filmorate.exception.ValidationException; -import ru.yandex.practicum.filmorate.model.User; - -import java.time.LocalDate; -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; - -@Slf4j -@Component("inMemoryUserStorage") -public class InMemoryUserStorage implements UserStorage { - - private final Map users = new HashMap<>(); - - @Override - public Collection findAll() { - return users.values(); - } - - @Override - public User create(User user) { - validateUser(user); - user.setId(getNextId()); - users.put(user.getId(), user); - return user; - } - - @Override - public User update(User newUser) { - if (newUser.getId() == null) { - log.warn("Не указан id"); - throw new ValidationException("Id должен быть указан"); - } - if (!users.containsKey(newUser.getId())) { - log.warn("Пользователь с указанным id не найден"); - throw new NotFoundException("Пользователь с id = " + newUser.getId() + " не найден"); - } - validateUser(newUser); - User oldUser = users.get(newUser.getId()); - oldUser.setEmail(newUser.getEmail()); - oldUser.setName(newUser.getName()); - oldUser.setLogin(newUser.getLogin()); - oldUser.setBirthday(newUser.getBirthday()); - return oldUser; - } - - private int getNextId() { - int currentMaxId = users.keySet() - .stream() - .mapToInt(id -> id) - .max() - .orElse(0); - return ++currentMaxId; - } - - public User findUserById(int userId) { - if (!users.containsKey(userId)) { - log.warn("Пользователь не найден"); - throw new NotFoundException("Пользователя с таким id не найдено"); - } - return users.get(userId); - } - - private void validateUser(User user) { - if (!user.getEmail().contains("@")) { - log.warn("Ошибка в формате почты"); - throw new ValidationException("Неверная почта"); - } - if (user.getBirthday().isAfter(LocalDate.now())) { - log.warn("Ошибка в дате рождения"); - throw new ValidationException("Неверная дата рождения"); - } - if (user.getLogin().contains(" ")) { - log.warn("Ошибка в формате логина"); - throw new ValidationException("Неправильный формат логина"); - } - if (user.getName() == null || user.getName().trim().isEmpty()) { - log.info("Пустое имя пользователя заменено на логин"); - user.setName(user.getLogin()); - } - } -} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/RatingStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/RatingStorage.java new file mode 100644 index 0000000..9c4d919 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/RatingStorage.java @@ -0,0 +1,13 @@ +package ru.yandex.practicum.filmorate.storage; + +import ru.yandex.practicum.filmorate.model.Rating; + +import java.util.Collection; +import java.util.Optional; + +public interface RatingStorage { + + Collection findAll(); + + Optional findRatingById(int id); +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/UserStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/UserStorage.java index 04935d0..ae1cea1 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/UserStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/UserStorage.java @@ -13,4 +13,16 @@ public interface UserStorage { User update(User newUser); User findUserById(int id); + + void remove(int id); + + void addFriend(int userId, int friendId); + + void deleteFriend(int userId, int friendId); + + Collection getFriendList(int userId); + + Collection getCommonFriendList(int id, int otherId); + + boolean contains(Integer id); } \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 4a23bbc..383a695 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,2 +1,7 @@ server.port=8080 -logging.level.org.zalando.logbook=TRACE \ No newline at end of file +logging.level.org.zalando.logbook=TRACE +spring.sql.init.mode=always +spring.datasource.url=jdbc:h2:file:./db/filmorate +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password=password \ No newline at end of file diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql new file mode 100644 index 0000000..6a974b6 --- /dev/null +++ b/src/main/resources/data.sql @@ -0,0 +1,20 @@ +DELETE FROM mpa; +DELETE FROM genres; + +ALTER TABLE genres ALTER COLUMN genre_id RESTART WITH 1; +ALTER TABLE mpa ALTER COLUMN mpa_id RESTART WITH 1; + +INSERT INTO genres (name) VALUES +('Комедия'), +('Драма'), +('Мультфильм'), +('Триллер'), +('Документальный'), +('Боевик'); + +INSERT INTO mpa (name) VALUES +('G'), +('PG'), +('PG-13'), +('R'), +('NC-17'); \ No newline at end of file diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql new file mode 100644 index 0000000..7cf15af --- /dev/null +++ b/src/main/resources/schema.sql @@ -0,0 +1,51 @@ +CREATE TABLE IF NOT EXISTS genres ( + genre_id INTEGER NOT NULL GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + name VARCHAR NOT NULL +); + +CREATE TABLE IF NOT EXISTS mpa ( + mpa_id INTEGER NOT NULL GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + name CHARACTER VARYING(50) NOT NULL +); + +CREATE TABLE IF NOT EXISTS users ( + user_id INTEGER NOT NULL GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + name CHARACTER VARYING(255) NOT NULL, + login CHARACTER VARYING(100) NOT NULL, + email CHARACTER VARYING(255) NOT NULL, + birthday DATE +); + +CREATE TABLE IF NOT EXISTS films ( + film_id INTEGER NOT NULL GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + name CHARACTER VARYING(255) NOT NULL, + description TEXT NOT NULL, + release_date DATE, + duration INTEGER NOT NULL, + mpa_id INTEGER NOT NULL, + FOREIGN KEY (mpa_id) REFERENCES mpa (mpa_id) ON DELETE SET NULL +); + +CREATE TABLE IF NOT EXISTS films_genres ( + film_id INTEGER NOT NULL, + genre_id INTEGER NOT NULL, + FOREIGN KEY (film_id) REFERENCES films (film_id) ON DELETE CASCADE, + FOREIGN KEY (genre_id) REFERENCES genres (genre_id) ON DELETE CASCADE, + PRIMARY KEY (film_id, genre_id) +); + +CREATE TABLE IF NOT EXISTS likes ( + film_id INTEGER NOT NULL, + user_id INTEGER NOT NULL, + FOREIGN KEY (film_id) REFERENCES films (film_id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES users (user_id) ON DELETE CASCADE, + PRIMARY KEY (film_id, user_id) +); + +CREATE TABLE IF NOT EXISTS user_friends ( + user_id INTEGER NOT NULL, + friend_id INTEGER NOT NULL, + FOREIGN KEY (friend_id) REFERENCES users (user_id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES users (user_id) ON DELETE CASCADE, + PRIMARY KEY (user_id, friend_id) +); \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/controller/InMemoryFilmStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/controller/InMemoryFilmStorageTest.java deleted file mode 100644 index 875d090..0000000 --- a/src/test/java/ru/yandex/practicum/filmorate/controller/InMemoryFilmStorageTest.java +++ /dev/null @@ -1,36 +0,0 @@ -package ru.yandex.practicum.filmorate.controller; - -import org.junit.jupiter.api.Test; -import ru.yandex.practicum.filmorate.exception.ValidationException; -import ru.yandex.practicum.filmorate.model.Film; -import ru.yandex.practicum.filmorate.storage.InMemoryFilmStorage; - -import static org.junit.jupiter.api.Assertions.*; - -import java.time.LocalDate; - -public class InMemoryFilmStorageTest { - InMemoryFilmStorage inMemoryFilmStorage = new InMemoryFilmStorage(); - - @Test - void getFilmTest() { - Film film = new Film(); - film.setReleaseDate(LocalDate.now()); - film.setDuration(100); - film.setName("test"); - film.setDescription("t"); - inMemoryFilmStorage.create(film); - film.setName(" "); - assertThrows(ValidationException.class, () -> inMemoryFilmStorage.update(film)); - film.setName("test"); - String description200 = "A".repeat(200); - film.setDescription(film.getDescription() + description200); - assertThrows(ValidationException.class, () -> inMemoryFilmStorage.update(film)); - film.setDescription("t"); - film.setReleaseDate(LocalDate.of(1895, 12, 27)); - assertThrows(ValidationException.class, () -> inMemoryFilmStorage.update(film)); - film.setReleaseDate(LocalDate.now()); - film.setDuration(0); - assertThrows(ValidationException.class, () -> inMemoryFilmStorage.update(film)); - } -} \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/controller/InMemoryUserStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/controller/InMemoryUserStorageTest.java deleted file mode 100644 index 3174a9a..0000000 --- a/src/test/java/ru/yandex/practicum/filmorate/controller/InMemoryUserStorageTest.java +++ /dev/null @@ -1,36 +0,0 @@ -package ru.yandex.practicum.filmorate.controller; - -import org.junit.jupiter.api.Test; -import ru.yandex.practicum.filmorate.exception.ValidationException; -import ru.yandex.practicum.filmorate.model.User; -import ru.yandex.practicum.filmorate.storage.InMemoryUserStorage; - -import java.time.LocalDate; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -public class InMemoryUserStorageTest { - InMemoryUserStorage inMemoryUserStorage = new InMemoryUserStorage(); - - @Test - void getFilmTest() { - User user = new User(); - user.setName("test1"); - user.setLogin("test2"); - user.setBirthday(LocalDate.of(2000, 12, 27)); - user.setEmail("test@"); - inMemoryUserStorage.create(user); - user.setEmail(" "); - assertThrows(ValidationException.class, () -> inMemoryUserStorage.update(user)); - user.setEmail("test2@"); - user.setLogin("TEST ETS"); - assertThrows(ValidationException.class, () -> inMemoryUserStorage.update(user)); - user.setLogin("test2"); - user.setName(" "); - inMemoryUserStorage.update(user); - assertEquals(user.getLogin(), user.getName(), "Имя совпадает с логином"); - user.setBirthday(LocalDate.of(2200, 12, 27)); - assertThrows(ValidationException.class, () -> inMemoryUserStorage.update(user)); - } -} \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/db/FilmDbStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/db/FilmDbStorageTest.java new file mode 100644 index 0000000..17ad066 --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/db/FilmDbStorageTest.java @@ -0,0 +1,98 @@ +package ru.yandex.practicum.filmorate.db; + +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.context.annotation.Import; +import org.springframework.dao.EmptyResultDataAccessException; +import ru.yandex.practicum.filmorate.dao.FilmDbStorage; +import ru.yandex.practicum.filmorate.dao.GenreDbStorage; +import ru.yandex.practicum.filmorate.dao.RatingDbStorage; +import ru.yandex.practicum.filmorate.dao.mappers.FilmRowMapper; +import ru.yandex.practicum.filmorate.dao.mappers.GenreRowMapper; +import ru.yandex.practicum.filmorate.dao.mappers.RatingRowMapper; +import ru.yandex.practicum.filmorate.model.Film; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +@JdbcTest +@Import({FilmDbStorage.class, FilmRowMapper.class, GenreDbStorage.class, GenreRowMapper.class, + RatingDbStorage.class, RatingRowMapper.class}) +@AutoConfigureTestDatabase +@RequiredArgsConstructor(onConstructor_ = @Autowired) +public class FilmDbStorageTest { + private final FilmDbStorage filmDbStorage; + private final RatingDbStorage ratingDbStorage; + + @Test + public void testFindFilmById() { + boolean id = filmDbStorage.contains(1); + assertTrue(id); + Film film = filmDbStorage.findFilmById(1); + assertNotNull(film); + assertEquals("name1", film.getName()); + } + + @Test + public void testFindAllFilms() { + List films = filmDbStorage.findAll(); + assertNotNull(films); + assertEquals(6, films.size()); + } + + @Test + public void testGetNotFound() { + boolean id = filmDbStorage.contains(10); + assertFalse(id); + assertThrows(EmptyResultDataAccessException.class,() -> filmDbStorage.findFilmById(10)); + } + + @Test + public void testAddFilm() { + Film film = new Film(); + film.setName("nam"); + film.setDescription("Desc"); + film.setDuration(60); + film.setMpa(ratingDbStorage.findRatingById(1).get()); + Film newFilm = filmDbStorage.create(film); + assertEquals(7, newFilm.getId()); + } + + @Test + public void testUpdateFilm() { + Film film = filmDbStorage.findFilmById(1); + film.setName("newFilmname"); + Film newFilm = filmDbStorage.update(film); + assertEquals("newFilmname", newFilm.getName()); + } + + @Test + public void testRemoveFilm() { + filmDbStorage.remove(6); + boolean id = filmDbStorage.contains(6); + assertFalse(id); + } + + @Test + public void testGetPopular() { + List popular = filmDbStorage.getPopular(3); + assertEquals(popular.get(0), filmDbStorage.findFilmById(3)); + assertEquals(popular.get(1), filmDbStorage.findFilmById(2)); + } + + @Test + public void testAddLikes() { + Integer likes = filmDbStorage.addLike(1, 4); + assertEquals(3, likes); + } + + @Test + public void testDeleteLikes() { + Integer likes = filmDbStorage.deleteLike(1, 1); + assertEquals(1, likes); + } +} diff --git a/src/test/java/ru/yandex/practicum/filmorate/db/GenreDbStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/db/GenreDbStorageTest.java new file mode 100644 index 0000000..a1c2970 --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/db/GenreDbStorageTest.java @@ -0,0 +1,77 @@ +package ru.yandex.practicum.filmorate.db; + +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.context.annotation.Import; +import ru.yandex.practicum.filmorate.dao.FilmDbStorage; +import ru.yandex.practicum.filmorate.dao.GenreDbStorage; +import ru.yandex.practicum.filmorate.dao.RatingDbStorage; +import ru.yandex.practicum.filmorate.dao.mappers.FilmRowMapper; +import ru.yandex.practicum.filmorate.dao.mappers.GenreRowMapper; +import ru.yandex.practicum.filmorate.dao.mappers.RatingRowMapper; +import ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.model.Genre; + +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@JdbcTest +@Import({FilmDbStorage.class, FilmRowMapper.class, GenreDbStorage.class, GenreRowMapper.class, + RatingDbStorage.class, RatingRowMapper.class}) +@AutoConfigureTestDatabase +@RequiredArgsConstructor(onConstructor_ = @Autowired) +public class GenreDbStorageTest { + private final GenreDbStorage genreDbStorage; + private final FilmDbStorage filmDbStorage; + + @Test + public void testFindRatingById() { + Optional genre = genreDbStorage.findGenreById(1); + assertTrue(genre.isPresent(), "Жанр есть"); + assertEquals("Комедия", genre.get().getName()); + } + + @Test + public void testFindAllRating() { + List genres = genreDbStorage.findAll(); + assertNotNull(genres); + assertEquals(6, genres.size()); + } + + @Test + public void testGetEmptyGenre() { + Optional genre = genreDbStorage.findGenreById(100); + assertThat(genre).isEmpty(); + } + + @Test + public void testAddGenreToFilm() { + Film film = filmDbStorage.findFilmById(2); + Set genres = film.getGenres(); + assertEquals(0, genres.size()); + + List allGenres = genreDbStorage.findAll(); + Set filmNewGenre = new HashSet<>(); + + filmNewGenre.add(allGenres.get(0)); + filmNewGenre.add(allGenres.get(1)); + + film.setGenres(filmNewGenre); + genreDbStorage.addGenresToFilm(film); + + Film newFilm = filmDbStorage.findFilmById(2); + Set newGenres = newFilm.getGenres(); + + assertEquals(2, newGenres.size()); + assertEquals(filmNewGenre, newGenres); + } +} \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/db/RatingDbStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/db/RatingDbStorageTest.java new file mode 100644 index 0000000..0029810 --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/db/RatingDbStorageTest.java @@ -0,0 +1,46 @@ +package ru.yandex.practicum.filmorate.db; + +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.context.annotation.Import; +import ru.yandex.practicum.filmorate.dao.RatingDbStorage; +import ru.yandex.practicum.filmorate.dao.mappers.RatingRowMapper; +import ru.yandex.practicum.filmorate.model.Rating; + +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +@JdbcTest +@Import({RatingDbStorage.class, RatingRowMapper.class}) +@AutoConfigureTestDatabase +@RequiredArgsConstructor(onConstructor_ = @Autowired) +public class RatingDbStorageTest { + + private final RatingDbStorage ratingDbStorage; + + @Test + public void testFindRatingById() { + Optional rating = ratingDbStorage.findRatingById(1); + assertTrue(rating.isPresent(), "Рейтинг есть"); + assertEquals("G", rating.get().getName()); + } + + @Test + public void testFindAllRating() { + List ratings = ratingDbStorage.findAll(); + assertNotNull(ratings); + assertEquals(5, ratings.size()); + } + + @Test + public void testGetEmptyRating() { + Optional rating = ratingDbStorage.findRatingById(100); + assertThat(rating).isEmpty(); + } +} \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/db/UserDbStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/db/UserDbStorageTest.java new file mode 100644 index 0000000..cd433a4 --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/db/UserDbStorageTest.java @@ -0,0 +1,109 @@ +package ru.yandex.practicum.filmorate.db; + +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.context.annotation.Import; +import org.springframework.dao.EmptyResultDataAccessException; +import ru.yandex.practicum.filmorate.dao.UserDbStorage; +import ru.yandex.practicum.filmorate.dao.mappers.UserRowMapper; +import ru.yandex.practicum.filmorate.model.User; + + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +@JdbcTest +@Import({UserDbStorage.class, UserRowMapper.class}) +@AutoConfigureTestDatabase +@RequiredArgsConstructor(onConstructor_ = @Autowired) +public class UserDbStorageTest { + private final UserDbStorage userStorage; + + @Test + public void testFindUserById() { + boolean id = userStorage.contains(1); + assertTrue(id); + User user = userStorage.findUserById(1); + assertNotNull(user); + assertEquals("name1", user.getName()); + } + + @Test + public void testFindAllUsers() { + List users = userStorage.findAll(); + assertNotNull(users); + assertEquals(5, users.size()); + } + + @Test + public void testGetNotFound() { + boolean id = userStorage.contains(10); + assertFalse(id); + assertThrows(EmptyResultDataAccessException.class,() -> userStorage.findUserById(10)); + } + + @Test + public void testAddUser() { + User user = new User(); + user.setName("nam"); + user.setLogin("log"); + user.setEmail("mail"); + User newUser = userStorage.create(user); + assertEquals(6, newUser.getId()); + } + + @Test + public void testUpdateUser() { + User user = userStorage.findUserById(1); + user.setName("newUsername"); + User newUser = userStorage.update(user); + assertEquals("newUsername", newUser.getName()); + } + + @Test + public void testRemoveUser() { + userStorage.remove(5); + boolean id = userStorage.contains(6); + assertFalse(id); + } + + @Test + public void testAddFriend() { + User user = userStorage.findUserById(1); + Integer id = user.getId(); + User friend = userStorage.findUserById(4); + Integer friendId = friend.getId(); + List friendList = userStorage.getFriendList(id); + assertEquals(1, friendList.size()); + userStorage.addFriend(id,friendId); + friendList = userStorage.getFriendList(id); + assertEquals(2, friendList.size()); + } + + @Test + public void testDeleteFriend() { + User user = userStorage.findUserById(1); + Integer id = user.getId(); + User friend = userStorage.findUserById(2); + Integer friendId = friend.getId(); + List friendList = userStorage.getFriendList(id); + assertEquals(1, friendList.size()); + userStorage.deleteFriend(id,friendId); + friendList = userStorage.getFriendList(id); + assertEquals(0, friendList.size()); + } + + @Test + public void testGetCommonFriends() { + User user1 = userStorage.findUserById(1); + Integer id1 = user1.getId(); + User user2 = userStorage.findUserById(4); + Integer id2 = user2.getId(); + List friendList = userStorage.getCommonFriendList(id1, id2); + assertEquals(1, friendList.size()); + } +} diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties new file mode 100644 index 0000000..941f85a --- /dev/null +++ b/src/test/resources/application.properties @@ -0,0 +1,8 @@ +spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1 +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password= + +spring.sql.init.mode=always +spring.sql.init.schema-locations=classpath:schema.sql +spring.sql.init.data-locations=classpath:data.sql \ No newline at end of file diff --git a/src/test/resources/data.sql b/src/test/resources/data.sql new file mode 100644 index 0000000..55b096a --- /dev/null +++ b/src/test/resources/data.sql @@ -0,0 +1,57 @@ +INSERT INTO mpa (name) VALUES +('G'), +('PG'), +('PG-13'), +('R'), +('NC-17'); + +INSERT INTO genres (name) VALUES +('Комедия'), +('Драма'), +('Мультфильм'), +('Триллер'), +('Документальный'), +('Боевик'); + +INSERT INTO users (email, login, name, birthday) VALUES +('email1', 'login1', 'name1', '2005-01-02'), +('email2', 'login2', 'name2', '2006-01-03'), +('email3', 'login3', 'name3', '2007-01-04'), +('email4', 'login4', 'name4', '2008-01-05'), +('email5', 'login5', 'name5', NULL); + +INSERT INTO user_friends(user_id, friend_id) VALUES +(1, 2), +(2, 1), +(3, 1), +(4, 2), +(3, 4); + +INSERT INTO films (name, description, release_date, duration, mpa_id) VALUES +('name1', 'description1', '2002-02-02', 100, 1), +('name2', 'description2', '2003-03-03', 90, 2), +('name3', 'description3', '2004-04-04', 150, 3), +('name4', 'description4', '2005-05-05', 140, 4), +('name5', 'description5', '2006-06-06', 120, 5), +('name6', 'description6', '2007-07-07', 120, 1); + +INSERT INTO films_genres (film_id, genre_id) VALUES +(4,1), +(3,2), +(3,3), +(5,1), +(5,2), +(5,3); + +INSERT INTO likes (film_id, user_id) VALUES +(1,1), +(1,5), +(2,1), +(2,3), +(2,4), +(3,2), +(3,3), +(3,1), +(3,4), +(3,5), +(6,4); \ No newline at end of file diff --git a/src/test/resources/schema.sql b/src/test/resources/schema.sql new file mode 100644 index 0000000..7cf15af --- /dev/null +++ b/src/test/resources/schema.sql @@ -0,0 +1,51 @@ +CREATE TABLE IF NOT EXISTS genres ( + genre_id INTEGER NOT NULL GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + name VARCHAR NOT NULL +); + +CREATE TABLE IF NOT EXISTS mpa ( + mpa_id INTEGER NOT NULL GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + name CHARACTER VARYING(50) NOT NULL +); + +CREATE TABLE IF NOT EXISTS users ( + user_id INTEGER NOT NULL GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + name CHARACTER VARYING(255) NOT NULL, + login CHARACTER VARYING(100) NOT NULL, + email CHARACTER VARYING(255) NOT NULL, + birthday DATE +); + +CREATE TABLE IF NOT EXISTS films ( + film_id INTEGER NOT NULL GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + name CHARACTER VARYING(255) NOT NULL, + description TEXT NOT NULL, + release_date DATE, + duration INTEGER NOT NULL, + mpa_id INTEGER NOT NULL, + FOREIGN KEY (mpa_id) REFERENCES mpa (mpa_id) ON DELETE SET NULL +); + +CREATE TABLE IF NOT EXISTS films_genres ( + film_id INTEGER NOT NULL, + genre_id INTEGER NOT NULL, + FOREIGN KEY (film_id) REFERENCES films (film_id) ON DELETE CASCADE, + FOREIGN KEY (genre_id) REFERENCES genres (genre_id) ON DELETE CASCADE, + PRIMARY KEY (film_id, genre_id) +); + +CREATE TABLE IF NOT EXISTS likes ( + film_id INTEGER NOT NULL, + user_id INTEGER NOT NULL, + FOREIGN KEY (film_id) REFERENCES films (film_id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES users (user_id) ON DELETE CASCADE, + PRIMARY KEY (film_id, user_id) +); + +CREATE TABLE IF NOT EXISTS user_friends ( + user_id INTEGER NOT NULL, + friend_id INTEGER NOT NULL, + FOREIGN KEY (friend_id) REFERENCES users (user_id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES users (user_id) ON DELETE CASCADE, + PRIMARY KEY (user_id, friend_id) +); \ No newline at end of file