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