diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/DirectorController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/DirectorController.java new file mode 100644 index 0000000..4e9b914 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/DirectorController.java @@ -0,0 +1,55 @@ +package ru.yandex.practicum.filmorate.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import ru.yandex.practicum.filmorate.dto.DirectorDto; +import ru.yandex.practicum.filmorate.model.Director; +import ru.yandex.practicum.filmorate.service.director.DirectorService; + +import java.util.Collection; + +@Slf4j +@RestController +@RequestMapping("/directors") +@RequiredArgsConstructor +@Validated +public class DirectorController { + + private final DirectorService directorService; + + @GetMapping + public Collection getDirectors() { + log.info("Received GET /directors request"); + return directorService.getDirectors(); + } + + @GetMapping("/{directorId}") + public DirectorDto getDirectorById(@PathVariable long directorId) { + log.info("Received GET /directors/{} request", directorId); + return directorService.getDirectorById(directorId); + } + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public DirectorDto addDirector(@RequestBody Director director) { + log.info("Received POST /directors request with body: {}", director); + return directorService.addDirector(director); + } + + @PutMapping + public DirectorDto updateDirector(@RequestBody Director director) { + log.info("Received PUT /directors request with body: {}", director); + return directorService.updateDirector(director); + } + + @DeleteMapping("/{directorId}") + public void deleteDirector(@PathVariable long directorId) { + log.info("Received DELETE /directors/{} request", directorId); + directorService.deleteDirector(directorId); + + } + +} 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 8baed43..8a9c7ab 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java @@ -84,4 +84,12 @@ public List getCommonFilms( log.info("GET common films for userId={} and friendId={}", userId, friendId); return filmService.getCommonFilms(userId, friendId); } + + @GetMapping("/director/{directorId}") + public List findByDirector(@PathVariable long directorId, + @RequestParam(name = "sortBy") String sortMode) { + log.info("Received GET /films/director/{}?sortBy={} request", directorId, sortMode); + return filmService.findByDirector(directorId, sortMode); + } + } \ No newline at end of file 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 d5c7121..9e5f8e5 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java @@ -5,8 +5,10 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; +import ru.yandex.practicum.filmorate.dto.FilmDto; import ru.yandex.practicum.filmorate.dto.UserDto; import ru.yandex.practicum.filmorate.model.User; +import ru.yandex.practicum.filmorate.service.film.FilmService; import ru.yandex.practicum.filmorate.service.user.UserService; import ru.yandex.practicum.filmorate.storage.user.UserDbStorage; @@ -17,10 +19,12 @@ @RequestMapping("/users") public class UserController { private final UserService userService; + private final FilmService filmService; @Autowired - public UserController(UserDbStorage userDbStorage) { + public UserController(UserDbStorage userDbStorage, FilmService filmService) { userService = new UserService(userDbStorage); + this.filmService = filmService; } @GetMapping @@ -79,5 +83,11 @@ public void deleteUser(@PathVariable int userId) { log.info("Recieved DELETE /users/{} request", userId); userService.deleteUser(userId); } + + @GetMapping("/{id}/recommendations") + public Collection getRecommendations(@PathVariable long id) { + log.info("get recommendations"); + return filmService.getRecommendations(id); + } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/DirectorRepository.java b/src/main/java/ru/yandex/practicum/filmorate/dal/DirectorRepository.java new file mode 100644 index 0000000..f18d332 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/DirectorRepository.java @@ -0,0 +1,48 @@ +package ru.yandex.practicum.filmorate.dal; + +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.model.Director; + +import java.util.List; +import java.util.Optional; + +@Repository +public class DirectorRepository extends BaseRepository { + + private static final String FIND_ALL_QUERY = "SELECT * FROM DIRECTORS d"; + private static final String FIND_BY_ID_QUERY = "SELECT * FROM DIRECTORS d WHERE d.director_id = ?"; + private static final String INSERT_QUERY = "INSERT INTO DIRECTORS (NAME) VALUES(?)"; + private static final String UPDATE_QUERY = "UPDATE DIRECTORS SET NAME = ? WHERE DIRECTOR_ID = ?"; + private static final String DELETE_QUERY = "DELETE FROM DIRECTORS WHERE DIRECTOR_ID = ?"; + + public DirectorRepository(JdbcTemplate jdbc, + RowMapper mapper) { + super(jdbc, mapper); + } + + public List findAll() { + return findMany(FIND_ALL_QUERY); + } + + public Optional findById(long id) { + return findOne(FIND_BY_ID_QUERY, id); + } + + public Director save(Director director) { + Long id = (Long) insert(INSERT_QUERY, director.getName()); + director.setId(id); + return director; + } + + public Director update(Director director) { + update(UPDATE_QUERY, director.getName(), director.getId()); + return director; + } + + public void delete(long id) { + update(DELETE_QUERY, id); + } + +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/FilmRepository.java b/src/main/java/ru/yandex/practicum/filmorate/dal/FilmRepository.java index 042be2e..e41c41e 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dal/FilmRepository.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/FilmRepository.java @@ -1,6 +1,8 @@ package ru.yandex.practicum.filmorate.dal; +import org.springframework.dao.DataAccessException; import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.ResultSetExtractor; import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; @@ -8,12 +10,15 @@ import org.springframework.jdbc.support.KeyHolder; import org.springframework.stereotype.Repository; import ru.yandex.practicum.filmorate.dal.mapper.FilmRowMapper; +import ru.yandex.practicum.filmorate.model.Director; import ru.yandex.practicum.filmorate.model.Film; import ru.yandex.practicum.filmorate.model.Genre; import ru.yandex.practicum.filmorate.model.Rating; import java.sql.Date; import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; import java.util.*; import java.util.stream.Collectors; @@ -31,24 +36,55 @@ public class FilmRepository extends BaseRepository { private static final String ADD_LIKE_QUERY = "INSERT INTO Likes (user_id, film_id) VALUES (?, ?)"; private static final String REMOVE_LIKE_QUERY = "DELETE FROM Likes WHERE film_id = ? AND user_id = ?"; private static final String GET_COMMON_FILMS = "SELECT f.*, r.rating_id as mpa_id, r.rating_name as mpa_name " + - "FROM Films f " + - "JOIN rating AS r ON f.rating_id = r.rating_id " + - "JOIN Likes l ON f.id = l.film_id " + - "WHERE l.film_id IN ( " + - " SELECT film_id FROM Likes WHERE user_id = ? " + - " INTERSECT " + - " SELECT film_id FROM Likes WHERE user_id = ? " + - ") " + - "GROUP BY f.id " + - "ORDER BY COUNT(l.user_id) DESC"; + "FROM Films f " + + "JOIN rating AS r ON f.rating_id = r.rating_id " + + "JOIN Likes l ON f.id = l.film_id " + + "WHERE l.film_id IN ( " + + " SELECT film_id FROM Likes WHERE user_id = ? " + + " INTERSECT " + + " SELECT film_id FROM Likes WHERE user_id = ? " + + ") " + + "GROUP BY f.id " + + "ORDER BY COUNT(l.user_id) DESC"; private static final String FIND_GENRES_FOR_FILMS_QUERY = "SELECT fg.film_id, g.id as genre_id, g.name as genre_name " + - "FROM genre g " + - "JOIN film_genre fg ON g.id = fg.genre_id " + - "WHERE fg.film_id IN (:filmIds)"; + "FROM genre g " + + "JOIN film_genre fg ON g.id = fg.genre_id " + + "WHERE fg.film_id IN (:filmIds)"; private static final String DELETE_FILM_QUERY = "DELETE FROM Films WHERE id = ?"; + private static final String FIND_DIRECTOR_FOR_FILMS_QUERY = """ + SELECT fd.FILM_ID, + d.DIRECTOR_ID, + d.NAME as DIRECTOR_NAME + FROM FILM_DIRECTORS fd + JOIN DIRECTORS d ON (fd.DIRECTOR_ID = d.DIRECTOR_ID) + WHERE fd.film_id in (:filmIds) + """; + + private static final String FIND_BY_DIRECTOR_SORT_BY_LIKES = """ + SELECT f.*, + r.rating_id as mpa_id, + r.rating_name as mpa_name + FROM films AS f + JOIN rating AS r ON f.rating_id = r.rating_id + JOIN FILM_DIRECTORS fd ON f.ID = fd.FILM_ID + WHERE fd.DIRECTOR_ID = ? + ORDER BY (SELECT COUNT(1) FROM LIKES l WHERE l.FILM_ID = f.ID) DESC + """; + + private static final String FIND_BY_DIRECTOR_SORT_BY_YEAR = """ + SELECT f.*, + r.rating_id as mpa_id, + r.rating_name as mpa_name + FROM films AS f + JOIN rating AS r ON f.rating_id = r.rating_id + JOIN FILM_DIRECTORS fd ON f.ID = fd.FILM_ID + WHERE fd.DIRECTOR_ID = ? + ORDER BY f.RELEASE_DATE + """; + private final JdbcTemplate jdbc; private final NamedParameterJdbcTemplate namedJdbcTemplate; private FilmRowMapper filmMapper; @@ -63,6 +99,16 @@ private static class FilmGenreRelation { } } + private static class FilmDirectorRelation { + final long filmId; + final Director director; + + FilmDirectorRelation(long filmId, Director director) { + this.filmId = filmId; + this.director = director; + } + } + private final RowMapper filmGenreRelationRowMapper = (rs, rowNum) -> { Genre genre = new Genre(); genre.setId(rs.getInt("genre_id")); @@ -70,6 +116,13 @@ private static class FilmGenreRelation { return new FilmGenreRelation(rs.getLong("film_id"), genre); }; + private final RowMapper filmDirectorRelationRowMapper = (rs, rowNum) -> { + Director director = new Director(); + director.setId(rs.getLong("director_id")); + director.setName(rs.getString("director_name")); + return new FilmDirectorRelation(rs.getLong("film_id"), director); + }; + public FilmRepository(JdbcTemplate jdbc, NamedParameterJdbcTemplate namedJdbcTemplate, FilmRowMapper filmMapper) { @@ -95,6 +148,7 @@ public List findAll() { if (!films.isEmpty()) { setGenresForFilms(films); + setDirectorForFilm(films); } return films; } @@ -107,6 +161,7 @@ public List findCommon(long userId, long filmId) { return films; } + public Optional findById(long id) { List films = jdbc.query(FIND_BY_ID_QUERY, filmWithRatingMapper, id); @@ -115,6 +170,7 @@ public Optional findById(long id) { } else { Film film = films.getFirst(); setGenresForFilms(List.of(film)); + setDirectorForFilm(List.of(film)); return Optional.of(film); } } @@ -168,7 +224,7 @@ public Film save(Film film) { film.setId(id); saveGenres(film); - + saveDirector(film); return findById(id).orElseThrow(() -> new IllegalStateException("Saved film not found, id: " + id)); } @@ -189,6 +245,9 @@ public Film update(Film film) { deleteGenres(film.getId()); saveGenres(film); + deleteFilms(film.getId()); + saveDirector(film); + return findById(film.getId()).orElseThrow(() -> new IllegalStateException("Updated film not found, id: " + film.getId())); } @@ -196,6 +255,25 @@ public boolean deleteFilm(long id) { return delete(DELETE_FILM_QUERY, id); } + public Map> getAllLikesGroupedByUser() { + String sql = "SELECT * FROM Likes"; + + return jdbc.query(sql, new ResultSetExtractor>>() { + @Override + public Map> extractData(ResultSet rs) throws SQLException, DataAccessException { + Map> userLikes = new HashMap<>(); + while (rs.next()) { + long userId = rs.getLong("user_id"); + long filmId = rs.getLong("film_id"); + + List filmIds = userLikes.computeIfAbsent(userId, k -> new ArrayList<>()); + filmIds.add(filmId); + } + return userLikes; + } + }); + } + private void deleteGenres(long filmId) { String sql = "DELETE FROM film_genre WHERE film_id = ?"; jdbc.update(sql, filmId); @@ -234,9 +312,10 @@ public void removeLike(long filmId, long userId) { public List getTopFilms(int count, Integer genreId, Integer year) { StringBuilder queryBuilder = new StringBuilder("SELECT f.*, r.rating_id AS mpa_id, r.rating_name AS mpa_name " + - "FROM films AS f JOIN rating AS r on f.rating_id = r.rating_id " + - "JOIN film_genre AS fg ON f.id = fg.film_id " + - "JOIN likes AS l ON f.id = l.film_id WHERE 1=1"); + "FROM films AS f JOIN rating AS r on f.rating_id = r.rating_id " + + "JOIN film_genre AS fg ON f.id = fg.film_id " + + "LEFT JOIN likes AS l ON f.id = l.film_id WHERE 1=1" + ); List filterParams = new ArrayList<>(); @@ -258,4 +337,75 @@ public List getTopFilms(int count, Integer genreId, Integer year) { return films; } + public List findByDirector(long directorId, String sortMode) { + String sql = ""; + if (sortMode.equals("likes")) { + sql = FIND_BY_DIRECTOR_SORT_BY_LIKES; + } else if (sortMode.equals("year")) { + sql = FIND_BY_DIRECTOR_SORT_BY_YEAR; + } + + List films = jdbc.query(sql, filmWithRatingMapper, directorId); + + if (!films.isEmpty()) { + setGenresForFilms(films); + setDirectorForFilm(films); + } + return films; + } + + private void saveDirector(Film film) { + if (film.getDirectors() == null || film.getDirectors().isEmpty()) { + return; + } + String sql = "INSERT INTO FILM_DIRECTORS (FILM_ID, DIRECTOR_ID) VALUES(?, ?)"; + + List batchArgs = film.getDirectors().stream() + .filter(Objects::nonNull) + .filter(director -> director.getId() > 0) + .distinct() + .map(director -> new Object[]{film.getId(), director.getId()}) + .collect(Collectors.toList()); + + if (!batchArgs.isEmpty()) { + jdbc.batchUpdate(sql, batchArgs); + } + } + + private void setDirectorForFilm(List films) { + List filmIds = films.stream() + .map(Film::getId) + .collect(Collectors.toList()); + + if (filmIds.isEmpty()) { + return; + } + + MapSqlParameterSource parameters = new MapSqlParameterSource(); + parameters.addValue("filmIds", filmIds); + + + List relations = namedJdbcTemplate.query( + FIND_DIRECTOR_FOR_FILMS_QUERY, + parameters, + filmDirectorRelationRowMapper + ); + + Map> directorsByFilmId = relations.stream() + .collect(groupingBy( + relation -> relation.filmId, + mapping(relation -> relation.director, toList()) + )); + + films.forEach(film -> + film.setDirectors(directorsByFilmId.getOrDefault(film.getId(), Collections.emptyList())) + ); + } + + private void deleteFilms(long filmId) { + String sql = "DELETE FROM FILM_DIRECTORS WHERE film_id = ?"; + jdbc.update(sql, filmId); + } + } + diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/mapper/DirectorRowMapper.java b/src/main/java/ru/yandex/practicum/filmorate/dal/mapper/DirectorRowMapper.java new file mode 100644 index 0000000..e306b93 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/mapper/DirectorRowMapper.java @@ -0,0 +1,20 @@ +package ru.yandex.practicum.filmorate.dal.mapper; + +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Component; +import ru.yandex.practicum.filmorate.model.Director; + +import java.sql.ResultSet; +import java.sql.SQLException; + +@Component +public class DirectorRowMapper implements RowMapper { + @Override + public Director mapRow(ResultSet rs, int rowNum) throws SQLException { + Director director = new Director(); + director.setId(rs.getLong("director_id")); + director.setName(rs.getString("name")); + + return director; + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/mapper/FilmRowMapper.java b/src/main/java/ru/yandex/practicum/filmorate/dal/mapper/FilmRowMapper.java index dcda2df..4c442a1 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dal/mapper/FilmRowMapper.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/mapper/FilmRowMapper.java @@ -27,6 +27,7 @@ public Film mapRow(ResultSet rs, int rowNum) throws SQLException { film.setMpa(mpa); film.setGenres(new ArrayList<>()); + film.setDirectors(new ArrayList<>()); return film; } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/DirectorDto.java b/src/main/java/ru/yandex/practicum/filmorate/dto/DirectorDto.java new file mode 100644 index 0000000..e617f0f --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/DirectorDto.java @@ -0,0 +1,9 @@ +package ru.yandex.practicum.filmorate.dto; + +import lombok.Data; + +@Data +public class DirectorDto { + private Long id; + private String name; +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/FilmDto.java b/src/main/java/ru/yandex/practicum/filmorate/dto/FilmDto.java index ccfb1ba..b9fe18d 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dto/FilmDto.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/FilmDto.java @@ -18,4 +18,5 @@ public class FilmDto { private RatingDto mpa; private List genres = new ArrayList<>(); private Set likes = new HashSet<>(); + private List directors = new ArrayList<>(); } \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/NewFilmRequest.java b/src/main/java/ru/yandex/practicum/filmorate/dto/NewFilmRequest.java index 9a18bba..1280050 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dto/NewFilmRequest.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/NewFilmRequest.java @@ -1,6 +1,7 @@ package ru.yandex.practicum.filmorate.dto; import lombok.Data; +import ru.yandex.practicum.filmorate.model.Director; import ru.yandex.practicum.filmorate.model.Rating; @Data @@ -10,4 +11,5 @@ public class NewFilmRequest { private String releaseDate; private String duration; private Rating ratingId; + private Director directors; } diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/UpdateFilmRequest.java b/src/main/java/ru/yandex/practicum/filmorate/dto/UpdateFilmRequest.java index d9fda56..44f24c0 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dto/UpdateFilmRequest.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/UpdateFilmRequest.java @@ -1,6 +1,7 @@ package ru.yandex.practicum.filmorate.dto; import lombok.Data; +import ru.yandex.practicum.filmorate.model.Director; import ru.yandex.practicum.filmorate.model.Rating; @Data @@ -10,6 +11,7 @@ public class UpdateFilmRequest { private String releaseDate; private String duration; private Rating ratingId; + private Director directors; public boolean hasName() { return !(name == null || name.isBlank()); diff --git a/src/main/java/ru/yandex/practicum/filmorate/exception/ParameterNotValidException.java b/src/main/java/ru/yandex/practicum/filmorate/exception/ParameterNotValidException.java new file mode 100644 index 0000000..19750f4 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/exception/ParameterNotValidException.java @@ -0,0 +1,4 @@ +package ru.yandex.practicum.filmorate.exception; + +public class ParameterNotValidException { +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/mappers/DirectorMapper.java b/src/main/java/ru/yandex/practicum/filmorate/mappers/DirectorMapper.java new file mode 100644 index 0000000..2e34790 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/mappers/DirectorMapper.java @@ -0,0 +1,36 @@ +package ru.yandex.practicum.filmorate.mappers; + +import lombok.experimental.UtilityClass; +import ru.yandex.practicum.filmorate.dto.DirectorDto; +import ru.yandex.practicum.filmorate.model.Director; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +@UtilityClass +public class DirectorMapper { + public static Director mapToDirector(Director request) { + Director director = new Director(); + director.setId(request.getId()); + director.setName(request.getName()); + return director; + } + + public static DirectorDto mapToDirectorDto(Director director) { + DirectorDto directorDto = new DirectorDto(); + directorDto.setId(director.getId()); + directorDto.setName(director.getName()); + return directorDto; + } + + public static List mapToDirectorDtoList(List directors) { + if (directors == null || directors.isEmpty()) { + return Collections.emptyList(); + } + return directors.stream() + .map(DirectorMapper::mapToDirectorDto) + .collect(Collectors.toList()); + } + +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/mappers/FilmMapper.java b/src/main/java/ru/yandex/practicum/filmorate/mappers/FilmMapper.java index 66c3506..e52c360 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/mappers/FilmMapper.java +++ b/src/main/java/ru/yandex/practicum/filmorate/mappers/FilmMapper.java @@ -2,6 +2,7 @@ import lombok.experimental.UtilityClass; import ru.yandex.practicum.filmorate.dto.FilmDto; +import ru.yandex.practicum.filmorate.model.Director; import ru.yandex.practicum.filmorate.model.Film; import ru.yandex.practicum.filmorate.model.Genre; @@ -33,6 +34,16 @@ public static Film mapToFilm(Film requestFilm) { film.setGenres(new ArrayList<>()); } + if (requestFilm.getDirectors() != null) { + List directorsWithIdOnly = requestFilm.getDirectors() + .stream() + .filter(d -> d != null && d.getId() != 0) + .collect(Collectors.toList()); + film.setDirectors(directorsWithIdOnly); + } else { + film.setDirectors(new ArrayList<>()); + } + return film; } @@ -48,6 +59,7 @@ public static FilmDto mapToFilmDto(Film film, Set likes) { dto.setMpa(RatingMapper.mapToRatingDto(film.getMpa())); dto.setGenres(GenreMapper.mapToGenreDtoList(film.getGenres())); dto.setLikes(Optional.ofNullable(likes).orElse(Collections.emptySet())); + dto.setDirectors(DirectorMapper.mapToDirectorDtoList(film.getDirectors())); return dto; } diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/Director.java b/src/main/java/ru/yandex/practicum/filmorate/model/Director.java new file mode 100644 index 0000000..e4e8ef4 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Director.java @@ -0,0 +1,9 @@ +package ru.yandex.practicum.filmorate.model; + +import lombok.Data; + +@Data +public class Director { + private Long id; + private String name; +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/Film.java b/src/main/java/ru/yandex/practicum/filmorate/model/Film.java index 79bf0c2..ca8c868 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/Film.java +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Film.java @@ -23,4 +23,5 @@ public class Film { @NotNull(message = "Rating cannot be null") private Rating mpa; private List genres = new ArrayList<>(); + private List directors = new ArrayList<>(); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/director/DirectorService.java b/src/main/java/ru/yandex/practicum/filmorate/service/director/DirectorService.java new file mode 100644 index 0000000..35aebbf --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/director/DirectorService.java @@ -0,0 +1,54 @@ +package ru.yandex.practicum.filmorate.service.director; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.dto.DirectorDto; +import ru.yandex.practicum.filmorate.mappers.DirectorMapper; +import ru.yandex.practicum.filmorate.model.Director; +import ru.yandex.practicum.filmorate.storage.director.DirectorDBStorage; + +import java.util.Collection; +import java.util.stream.Collectors; + +@Slf4j +@Service +@RequiredArgsConstructor +public class DirectorService { + + private final DirectorDBStorage directorStorage; + + public Collection getDirectors() { + log.info("Getting all directors"); + Collection directors = directorStorage.getDirectors(); + return directors.stream() + .map(DirectorMapper::mapToDirectorDto) + .collect(Collectors.toList()); + } + + public DirectorDto getDirectorById(long directorId) { + log.info("Getting director by id: {}", directorId); + Director director = directorStorage.getDirectorById(directorId); + return DirectorMapper.mapToDirectorDto(director); + } + + public DirectorDto addDirector(Director director) { + log.info("Adding new director: {}", director.getName()); + Director newDirector = directorStorage.addDirector(director); + log.info("Director {} added with id: {}", director.getName(), director.getId()); + return DirectorMapper.mapToDirectorDto(director); + } + + public DirectorDto updateDirector(Director director) { + log.info("Updating director with id: {}", director.getId()); + Director updateDirector = directorStorage.updateDirector(director); + log.info("Director {} with id: {} updated", updateDirector.getName(), updateDirector.getId()); + return DirectorMapper.mapToDirectorDto(updateDirector); + } + + public void deleteDirector(long directorId) { + log.info("Deleting director by id: {}", directorId); + directorStorage.deleteDirector(directorId); + } + +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/film/FilmService.java b/src/main/java/ru/yandex/practicum/filmorate/service/film/FilmService.java index 020dfb3..d5929f4 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/film/FilmService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/film/FilmService.java @@ -94,7 +94,6 @@ public List getTopFilms(int count, int genreId, int year) { if (count <= 0) { throw new ValidationException("Count parameter must be positive."); } - return filmStorage.getTopFilms(count, genreId, year) .stream() .map(film -> { @@ -159,6 +158,15 @@ public void deleteFilm(long filmId) { } + public List getRecommendations(long id) { + userExists(id); + log.info("Getting recommendations films for user with id = {}", id); + Collection films = filmStorage.getRecommendations(id); + return films.stream() + .map(film -> FilmMapper.mapToFilmDto(film, filmStorage.getLikes(film.getId()))) + .collect(Collectors.toList()); + } + private void validateFilm(Film film) { if (film.getReleaseDate() != null && film.getReleaseDate().isBefore(MIN_RELEASE_DATE)) { log.warn("Validation failed"); @@ -173,4 +181,13 @@ private void userExists(long userId) { private void filmExists(long filmId) { filmStorage.getFilmById(filmId); } + + public List findByDirector(long directorId, String sortMode) { + log.info("Getting films by director with id: {} and sort mode {}", directorId, sortMode); + Collection films = filmStorage.findByDirector(directorId, sortMode); + return films.stream() + .map(film -> FilmMapper.mapToFilmDto(film, filmStorage.getLikes(film.getId()))) + .collect(Collectors.toList()); + } + } \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/director/DirectorDBStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/director/DirectorDBStorage.java new file mode 100644 index 0000000..750de3a --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/director/DirectorDBStorage.java @@ -0,0 +1,43 @@ +package ru.yandex.practicum.filmorate.storage.director; + +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.dal.DirectorRepository; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.model.Director; + +import java.util.Collection; + +@Repository +public class DirectorDBStorage { + private final DirectorRepository directorRepository; + + public DirectorDBStorage(DirectorRepository directorRepository) { + this.directorRepository = directorRepository; + } + + public Director addDirector(Director director) { + return directorRepository.save(director); + } + + public Director updateDirector(Director director) { + directorRepository.findById(director.getId()) + .orElseThrow(() -> new NotFoundException("Director with id " + director.getId() + " not found")); + return directorRepository.update(director); + } + + public Collection getDirectors() { + return directorRepository.findAll(); + } + + public Director getDirectorById(long directorId) { + return directorRepository.findById(directorId) + .orElseThrow(() -> new NotFoundException("Director with id " + directorId + " not found")); + } + + public void deleteDirector(long directorId) { + directorRepository.findById(directorId) + .orElseThrow(() -> new NotFoundException("Director with id " + directorId + " not found")); + directorRepository.delete(directorId); + } +} + diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java index 7d18f16..cd8de11 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java @@ -9,9 +9,8 @@ import ru.yandex.practicum.filmorate.model.Genre; import ru.yandex.practicum.filmorate.model.Rating; -import java.util.Collection; -import java.util.List; -import java.util.Set; +import java.util.*; +import java.util.stream.Collectors; @Repository public class FilmDbStorage implements FilmStorage { @@ -123,4 +122,97 @@ private void validateGenresExist(List genres) { } } } + + public Collection getRecommendations(long targetUserId) { + Optional mostSimilarUserOpt = findUserWithMostCommonLikes(targetUserId); + + if (mostSimilarUserOpt.isEmpty()) { + return Collections.emptyList(); + } + long mostSimilarUserId = mostSimilarUserOpt.get(); + + Map> allUserLikes = filmRepository.getAllLikesGroupedByUser(); + + Set filmsLikedByTarget = new HashSet<>(allUserLikes.getOrDefault(targetUserId, Collections.emptyList())); + + List filmsLikedBySimilar = allUserLikes.getOrDefault(mostSimilarUserId, Collections.emptyList()); + + List recommendedFilmIds = filmsLikedBySimilar.stream() + .filter(filmId -> !filmsLikedByTarget.contains(filmId)) + .collect(Collectors.toList()); + + if (recommendedFilmIds.isEmpty()) { + return Collections.emptyList(); + } + + return recommendedFilmIds.stream() + .map(filmId -> { + try { + return getFilmById(filmId); + } catch (NotFoundException e) { + return null; + } + }) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + public Optional findUserWithMostCommonLikes(long id) { + Map crossesFilms = findCrosses(id); + + if (crossesFilms == null || crossesFilms.isEmpty()) { + return Optional.empty(); + } + + Optional> maxEntry = crossesFilms.entrySet() + .stream() + .max(Map.Entry.comparingByValue()); + + return maxEntry.map(Map.Entry::getKey); + } + + + private Map findCrosses(long targetUserId) { + Map> allUserLikes = filmRepository.getAllLikesGroupedByUser(); + + List filmsLikedByTargetUser = allUserLikes.get(targetUserId); + + if (filmsLikedByTargetUser == null || filmsLikedByTargetUser.isEmpty()) { + return Collections.emptyMap(); + } + + Set targetLikedSet = new HashSet<>(filmsLikedByTargetUser); + + Map commonLikesCountMap = new HashMap<>(); + + for (Map.Entry> entry : allUserLikes.entrySet()) { + long otherUserId = entry.getKey(); + List filmsLikedByOtherUser = entry.getValue(); + + if (otherUserId == targetUserId) { + continue; + } + + if (filmsLikedByOtherUser == null || filmsLikedByOtherUser.isEmpty()) { + continue; + } + + int commonCount = 0; + for (Long filmLikedByOther : filmsLikedByOtherUser) { + if (targetLikedSet.contains(filmLikedByOther)) { + commonCount++; + } + } + + if (commonCount > 0) { + commonLikesCountMap.put(otherUserId, commonCount); + } + } + return commonLikesCountMap; + } + + public List findByDirector(long directorId, String sortMode) { + return filmRepository.findByDirector(directorId, sortMode); + } + } \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 73fd17b..2776f53 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,4 +1,4 @@ -logging.level.org.zalando.logbook=TRACE +logging.level.org.zalando.logbook=INFO logging.level.ru.yandex.practicum.filmorate=INFO spring.sql.init.mode=always spring.datasource.url=jdbc:h2:file:./db/filmorate diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index c7bdbc0..ec1f723 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -1,42 +1,42 @@ -DELETE -FROM FILMS; -DELETE -FROM LIKES; -DELETE -FROM GENRE; -DELETE -FROM RATING; -DELETE -FROM USERS; -DELETE -FROM FILM_GENRE; -DELETE -FROM FRIENDSHIP; +delete +from FILMS; +delete +from LIKES; +delete +from GENRE; +delete +from RATING; +delete +from USERS; +delete +from FILM_GENRE; +delete +from FRIENDSHIP; -INSERT INTO RATING(rating_id, rating_name) -VALUES (1, 'G'), +insert into RATING(rating_id, rating_name) +values (1, 'G'), (2, 'PG'), (3, 'PG-13'), (4, 'R'), (5, 'NC-17'); -INSERT INTO USERS (email, login, name, birthday) -VALUES ('test@user.com', 'testlogin', 'Test User Name', '1991-11-11'); +insert into USERS (email, login, name, birthday) +values ('test@user.com', 'testlogin', 'Test User Name', '1991-11-11'); -INSERT INTO USERS ( email, login, name, birthday) -VALUES ('test1@user.com', 'test2login', 'Another User', '1992-11-11'); +insert into USERS ( email, login, name, birthday) +values ('test1@user.com', 'test2login', 'Another User', '1992-11-11'); -INSERT INTO FILMS (NAME, DESCRIPTION, RELEASE_DATE, DURATION, RATING_ID) -VALUES ('film1', 'its film1', '2010-10-10', '100', '1'), +insert into FILMS (NAME, DESCRIPTION, RELEASE_DATE, DURATION, RATING_ID) +values ('film1', 'its film1', '2010-10-10', '100', '1'), ('film2', 'its film2', '2020-12-20', '200', '2'); -INSERT INTO LIKES(user_id, film_id) -VALUES (1, 1), +insert into LIKES(user_id, film_id) +values (1, 1), (2, 1), (2, 2); -INSERT INTO GENRE (name) -VALUES ('Комедия'), +insert into GENRE (name) +values ('Комедия'), ('Драма'), ('Мультфильм'), ('Триллер'), diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index c290170..289841c 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -55,4 +55,21 @@ CREATE TABLE IF NOT EXISTS Film_genre PRIMARY KEY (film_id, genre_id), FOREIGN KEY (film_id) REFERENCES Films (id) ON DELETE CASCADE, FOREIGN KEY (genre_id) REFERENCES Genre (id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS directors +( + director_id BIGINT NOT NULL GENERATED BY DEFAULT AS IDENTITY, + name varchar(255) NOT NULL, + CONSTRAINT directors_pk PRIMARY KEY (director_id) +); + +CREATE TABLE IF NOT EXISTS film_directors +( + id BIGINT NOT NULL GENERATED BY DEFAULT AS IDENTITY, + film_id BIGINT NOT NULL, + director_id BIGINT NOT NULL, + CONSTRAINT film_directors_pk PRIMARY KEY (id), + FOREIGN KEY (film_id) REFERENCES Films (id) ON DELETE CASCADE, + FOREIGN KEY (director_id) REFERENCES directors (director_id) ON DELETE CASCADE ); \ No newline at end of file