From 135ae8f02750627607ceb955e2a0363ff07d73dd Mon Sep 17 00:00:00 2001 From: Ksenia Date: Fri, 29 May 2026 22:03:20 +0300 Subject: [PATCH 01/29] feat: add likes field to Film and friends field to User models --- src/main/java/ru/yandex/practicum/filmorate/model/Film.java | 4 ++++ src/main/java/ru/yandex/practicum/filmorate/model/User.java | 4 ++++ 2 files changed, 8 insertions(+) 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 6b01356..de7bc71 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/Film.java +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Film.java @@ -7,6 +7,8 @@ import lombok.Data; import java.time.LocalDate; +import java.util.HashSet; +import java.util.Set; /** * Film. @@ -27,4 +29,6 @@ public class Film { @NotNull(message = "Продолжительность фильма не может быть пустой") @Positive(message = "Продолжительность фильма должна быть положительной") private Integer duration; + + private Set likes = new HashSet<>(); } 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 33003b4..8b35be3 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/User.java +++ b/src/main/java/ru/yandex/practicum/filmorate/model/User.java @@ -7,6 +7,8 @@ import lombok.Data; import java.time.LocalDate; +import java.util.HashSet; +import java.util.Set; @Data public class User { @@ -24,4 +26,6 @@ public class User { @NotNull(message = "Дата рождения не может быть пустой") @PastOrPresent(message = "Дата рождения не может быть в будущем") private LocalDate birthday; + + private Set friends = new HashSet<>(); } From 5b73b1d9bed3cc10a0ff5167eccb72fba192b527 Mon Sep 17 00:00:00 2001 From: Ksenia Date: Fri, 29 May 2026 22:10:56 +0300 Subject: [PATCH 02/29] feat: add storage layer with FilmStorage and UserStorage interfaces --- .../exception/NotFoundException.java | 7 ++ .../filmorate/storage/film/FilmStorage.java | 18 +++++ .../storage/film/InMemoryFilmStorage.java | 62 +++++++++++++++++ .../storage/user/InMemoryUserStorage.java | 67 +++++++++++++++++++ .../filmorate/storage/user/UserStorage.java | 18 +++++ 5 files changed, 172 insertions(+) create mode 100644 src/main/java/ru/yandex/practicum/filmorate/exception/NotFoundException.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmStorage.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/user/UserStorage.java diff --git a/src/main/java/ru/yandex/practicum/filmorate/exception/NotFoundException.java b/src/main/java/ru/yandex/practicum/filmorate/exception/NotFoundException.java new file mode 100644 index 0000000..82e3f72 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/exception/NotFoundException.java @@ -0,0 +1,7 @@ +package ru.yandex.practicum.filmorate.exception; + +public class NotFoundException extends RuntimeException { + public NotFoundException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmStorage.java new file mode 100644 index 0000000..40e56d2 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmStorage.java @@ -0,0 +1,18 @@ +package ru.yandex.practicum.filmorate.storage.film; + +import ru.yandex.practicum.filmorate.model.Film; + +import java.util.Collection; + +public interface FilmStorage { + + Film add(Film film); + + Film update(Film film); + + Film delete(long id); + + Collection findAll(); + + Film findById(long id); +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java new file mode 100644 index 0000000..b6206e0 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java @@ -0,0 +1,62 @@ +package ru.yandex.practicum.filmorate.storage.film; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.model.Film; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +@Component +@Slf4j +public class InMemoryFilmStorage implements FilmStorage { + + // Хранилище фильмов в памяти + private final Map films = new HashMap<>(); + + private long currentId = 1; + + @Override + public Film add(Film film) { + film.setId(currentId++); + films.put(film.getId(), film); + log.debug("Добавлен фильм: id={}, name={}", film.getId(), film.getName()); + return film; + } + + @Override + public Film update(Film film) { + if (!films.containsKey(film.getId())) { + log.warn("Попытка обновить несуществующий фильм: id={}", film.getId()); + throw new NotFoundException("Фильм с id=" + film.getId() + " не найден"); + } + films.put(film.getId(), film); + log.debug("Обновлён фильм: id={}", film.getId()); + return film; + } + + @Override + public Film delete(long id) { + Film film = findById(id); + films.remove(id); + log.debug("Удалён фильм: id={}", id); + return film; + } + + @Override + public Collection findAll() { + return films.values(); + } + + @Override + public Film findById(long id) { + Film film = films.get(id); + if (film == null) { + log.warn("Фильм не найден: id={}", id); + throw new NotFoundException("Фильм с id=" + id + " не найден"); + } + return film; + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java new file mode 100644 index 0000000..d24167a --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java @@ -0,0 +1,67 @@ +package ru.yandex.practicum.filmorate.storage.user; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.model.User; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +@Component +@Slf4j +public class InMemoryUserStorage implements UserStorage { + + private final Map users = new HashMap<>(); + private long currentId = 1; + + @Override + public User add(User user) { + // Если имя пустое — используем логин (бизнес-правило из прошлого спринта) + if (user.getName() == null || user.getName().isBlank()) { + user.setName(user.getLogin()); + } + user.setId(currentId++); + users.put(user.getId(), user); + log.debug("Добавлен пользователь: id={}, login={}", user.getId(), user.getLogin()); + return user; + } + + @Override + public User update(User user) { + if (!users.containsKey(user.getId())) { + log.warn("Попытка обновить несуществующего пользователя: id={}", user.getId()); + throw new NotFoundException("Пользователь с id=" + user.getId() + " не найден"); + } + if (user.getName() == null || user.getName().isBlank()) { + user.setName(user.getLogin()); + } + users.put(user.getId(), user); + log.debug("Обновлён пользователь: id={}", user.getId()); + return user; + } + + @Override + public User delete(long id) { + User user = findById(id); + users.remove(id); + log.debug("Удалён пользователь: id={}", id); + return user; + } + + @Override + public Collection findAll() { + return users.values(); + } + + @Override + public User findById(long id) { + User user = users.get(id); + if (user == null) { + log.warn("Пользователь не найден: id={}", id); + throw new NotFoundException("Пользователь с id=" + id + " не найден"); + } + return user; + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserStorage.java new file mode 100644 index 0000000..1412132 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserStorage.java @@ -0,0 +1,18 @@ +package ru.yandex.practicum.filmorate.storage.user; + +import ru.yandex.practicum.filmorate.model.User; + +import java.util.Collection; + +public interface UserStorage { + + User add(User user); + + User update(User user); + + User delete(long id); + + Collection findAll(); + + User findById(long id); +} \ No newline at end of file From 228c9607707d96aa68055ac19a67b07865a2d800 Mon Sep 17 00:00:00 2001 From: Ksenia Date: Fri, 29 May 2026 22:19:14 +0300 Subject: [PATCH 03/29] feat: add FilmService and UserService with business logic --- .../filmorate/service/FilmService.java | 85 ++++++++++++++++++ .../filmorate/service/UserService.java | 89 +++++++++++++++++++ 2 files changed, 174 insertions(+) create mode 100644 src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/service/UserService.java diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java b/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java new file mode 100644 index 0000000..2e0e6d3 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java @@ -0,0 +1,85 @@ +package ru.yandex.practicum.filmorate.service; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.storage.film.FilmStorage; +import ru.yandex.practicum.filmorate.storage.user.UserStorage; + +import java.util.*; + +@Service +@Slf4j +public class FilmService { + + private final FilmStorage filmStorage; + // Нужен для проверки существования пользователя, который ставит лайк + private final UserStorage userStorage; + + @Autowired + public FilmService(FilmStorage filmStorage, UserStorage userStorage) { + this.filmStorage = filmStorage; + this.userStorage = userStorage; + } + + public Film addLike(long filmId, long userId) { + Film film = filmStorage.findById(filmId); + // Проверка существования пользователя — иначе 404 + userStorage.findById(userId); + + film.getLikes().add(userId); + log.debug("Пользователь id={} поставил лайк фильму id={}", userId, filmId); + return film; + } + + public Film removeLike(long filmId, long userId) { + Film film = filmStorage.findById(filmId); + // Проверка существования пользователя — иначе 404 + userStorage.findById(userId); + + if (!film.getLikes().contains(userId)) { + throw new NotFoundException( + "Пользователь id=" + userId + " не ставил лайк фильму id=" + filmId + ); + } + + film.getLikes().remove(userId); + log.debug("Пользователь id={} убрал лайк с фильма id={}", userId, filmId); + return film; + } + + public Collection getPopular(int count) { + List allFilms = new ArrayList<>(filmStorage.findAll()); + + // Сортировка по убыванию количества лайков + Collections.sort(allFilms, new Comparator() { + @Override + public int compare(Film f1, Film f2) { + return f2.getLikes().size() - f1.getLikes().size(); + } + }); + + // Возвращение не больше count фильмов + int resultSize = Math.min(count, allFilms.size()); + return allFilms.subList(0, resultSize); + } + + // Делегирование базовых операций хранилищу + public Film add(Film film) { + return filmStorage.add(film); + } + + public Film update(Film film) { + return filmStorage.update(film); + } + + public Collection findAll() { + return filmStorage.findAll(); + } + + public Film findById(long id) { + return filmStorage.findById(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 new file mode 100644 index 0000000..e1b2a04 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java @@ -0,0 +1,89 @@ +package ru.yandex.practicum.filmorate.service; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.model.User; +import ru.yandex.practicum.filmorate.storage.user.UserStorage; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +@Service +@Slf4j +public class UserService { + + private final UserStorage userStorage; + + @Autowired + public UserService(UserStorage userStorage) { + this.userStorage = userStorage; + } + + public User addFriend(long userId, long friendId) { + User user = userStorage.findById(userId); + User friend = userStorage.findById(friendId); + + user.getFriends().add(friendId); + friend.getFriends().add(userId); + + log.debug("Пользователи id={} и id={} теперь друзья", userId, friendId); + return user; + } + + public User removeFriend(long userId, long friendId) { + User user = userStorage.findById(userId); + User friend = userStorage.findById(friendId); + + user.getFriends().remove(friendId); + friend.getFriends().remove(userId); + + log.debug("Пользователи id={} и id={} больше не друзья", userId, friendId); + return user; + } + + public Collection getFriends(long userId) { + User user = userStorage.findById(userId); + List friends = new ArrayList<>(); + + for (Long friendId : user.getFriends()) { + friends.add(userStorage.findById(friendId)); + } + + return friends; + } + + public Collection getCommonFriends(long userId, long otherId) { + User user = userStorage.findById(userId); + User other = userStorage.findById(otherId); + List common = new ArrayList<>(); + + for (Long friendId : user.getFriends()) { + // Если этот друг есть и у другого пользователя = общий + if (other.getFriends().contains(friendId)) { + common.add(userStorage.findById(friendId)); + } + } + + return common; + } + + // Делегирование базовых операций хранилищу + public User add(User user) { + return userStorage.add(user); + } + + public User update(User user) { + return userStorage.update(user); + } + + public Collection findAll() { + return userStorage.findAll(); + } + + public User findById(long id) { + return userStorage.findById(id); + } +} \ No newline at end of file From da04739e5617ab3463be9561efc7ee7cbf969984 Mon Sep 17 00:00:00 2001 From: Ksenia Date: Fri, 29 May 2026 22:21:18 +0300 Subject: [PATCH 04/29] feat: refactor controllers to use service layer, add new REST endpoints --- .../filmorate/controller/FilmController.java | 76 ++++++++--------- .../filmorate/controller/UserController.java | 84 +++++++++---------- 2 files changed, 74 insertions(+), 86 deletions(-) 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 b94fd78..21c3dda 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java @@ -2,73 +2,65 @@ import jakarta.validation.Valid; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; -import ru.yandex.practicum.filmorate.exception.ValidationException; import ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.service.FilmService; -import java.time.LocalDate; import java.util.Collection; -import java.util.HashMap; -import java.util.Map; @RestController @RequestMapping("/films") @Slf4j public class FilmController { - // Хранилище фильмов в памяти - private final Map films = new HashMap<>(); + private final FilmService filmService; - private long currentId = 1; - - // Константа: дата первого релиза (день рождения кино) - private static final LocalDate CINEMA_BIRTHDAY = LocalDate.of(1895, 12, 28); + @Autowired + public FilmController(FilmService filmService) { + this.filmService = filmService; + } @GetMapping public Collection findAll() { - log.info("Получен запрос на получение списка всех фильмов"); - return films.values(); + log.info("GET /films"); + return filmService.findAll(); + } + + @GetMapping("/{id}") + public Film findById(@PathVariable long id) { + log.info("GET /films/{}", id); + return filmService.findById(id); } @PostMapping public Film create(@Valid @RequestBody Film film) { - log.info("Получен запрос на создание фильма: {}", film); - - validateFilm(film); - - film.setId(getNextId()); - films.put(film.getId(), film); - - log.info("Фильм успешно добавлен. Присвоен ID: {}", film.getId()); - return film; + log.info("POST /films : {}", film.getName()); + return filmService.add(film); } @PutMapping public Film update(@Valid @RequestBody Film film) { - log.info("Получен запрос на обновление фильма с ID: {}", film.getId()); - - if (film.getId() == null || !films.containsKey(film.getId())) { - log.error("Ошибка обновления: фильм с ID {} не найден", film.getId()); - throw new ValidationException("Фильм с таким ID не найден"); - } - - validateFilm(film); + log.info("PUT /films : id={}", film.getId()); + return filmService.update(film); + } - films.put(film.getId(), film); - log.info("Фильм с ID {} успешно обновлен", film.getId()); - return film; + @PutMapping("/{id}/like/{userId}") + public Film addLike(@PathVariable long id, @PathVariable long userId) { + log.info("PUT /films/{}/like/{}", id, userId); + return filmService.addLike(id, userId); } - // Вспомогательный метод для валидации - private void validateFilm(Film film) { - if (film.getReleaseDate().isBefore(CINEMA_BIRTHDAY)) { - log.error("Ошибка валидации: дата релиза {} раньше 28 декабря 1895 года", film.getReleaseDate()); - throw new ValidationException("Дата релиза фильма не может быть раньше 28 декабря 1895 года"); - } + @DeleteMapping("/{id}/like/{userId}") + public Film removeLike(@PathVariable long id, @PathVariable long userId) { + log.info("DELETE /films/{}/like/{}", id, userId); + return filmService.removeLike(id, userId); } - // Вспомогательный метод генерации id - private long getNextId() { - return currentId++; + @GetMapping("/popular") + public Collection getPopular( + @RequestParam(defaultValue = "10") int count) { + log.info("GET /films/popular?count={}", count); + return filmService.getPopular(count); } -} +} \ 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 f7ca087..ba0d0ec 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java @@ -2,76 +2,72 @@ import jakarta.validation.Valid; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; -import ru.yandex.practicum.filmorate.exception.ValidationException; import ru.yandex.practicum.filmorate.model.User; +import ru.yandex.practicum.filmorate.service.UserService; import java.util.Collection; -import java.util.HashMap; -import java.util.Map; @RestController @RequestMapping("/users") @Slf4j public class UserController { - // Хранилище пользователей в памяти - private final Map users = new HashMap<>(); + private final UserService userService; - private long currentId = 1; + @Autowired + public UserController(UserService userService) { + this.userService = userService; + } @GetMapping public Collection findAll() { - log.info("Получен запрос на получение списка всех пользователей"); - return users.values(); + log.info("GET /users"); + return userService.findAll(); + } + + @GetMapping("/{id}") + public User findById(@PathVariable long id) { + log.info("GET /users/{}", id); + return userService.findById(id); } @PostMapping public User create(@Valid @RequestBody User user) { - log.info("Получен запрос на создание пользователя: {}", user.getLogin()); - - validateUser(user); - - user.setId(getNextId()); - users.put(user.getId(), user); - - log.info("Пользователь успешно добавлен. Присвоен ID: {}", user.getId()); - return user; + log.info("POST /users : {}", user.getLogin()); + return userService.add(user); } @PutMapping public User update(@Valid @RequestBody User user) { - log.info("Получен запрос на обновление пользователя с ID: {}", user.getId()); - - if (user.getId() == null || !users.containsKey(user.getId())) { - log.error("Ошибка обновления: пользователь с ID {} не найден", user.getId()); - throw new ValidationException("Пользователь с таким ID не найден"); - } - - validateUser(user); + log.info("PUT /users : id={}", user.getId()); + return userService.update(user); + } - users.put(user.getId(), user); - log.info("Пользователь с ID {} успешно обновлен", user.getId()); - return user; + @PutMapping("/{id}/friends/{friendId}") + public User addFriend(@PathVariable long id, @PathVariable long friendId) { + log.info("PUT /users/{}/friends/{}", id, friendId); + return userService.addFriend(id, friendId); } - // Вспомогательный метод для валидации и бизнес-логики - private void validateUser(User user) { - // Проверка на пробелы в логине - if (user.getLogin().contains(" ")) { - log.error("Ошибка валидации: логин '{}' содержит пробелы", user.getLogin()); - throw new ValidationException("Логин не может содержать пробелы"); - } + @DeleteMapping("/{id}/friends/{friendId}") + public User removeFriend(@PathVariable long id, @PathVariable long friendId) { + log.info("DELETE /users/{}/friends/{}", id, friendId); + return userService.removeFriend(id, friendId); + } - // Если имя пустое = использование логина - if (user.getName() == null || user.getName().isBlank()) { - log.info("Имя пользователя не указано. Используем логин: {}", user.getLogin()); - user.setName(user.getLogin()); - } + @GetMapping("/{id}/friends") + public Collection getFriends(@PathVariable long id) { + log.info("GET /users/{}/friends", id); + return userService.getFriends(id); } - // Вспомогательный метод для генерации ID - private long getNextId() { - return currentId++; + @GetMapping("/{id}/friends/common/{otherId}") + public Collection getCommonFriends( + @PathVariable long id, + @PathVariable long otherId) { + log.info("GET /users/{}/friends/common/{}", id, otherId); + return userService.getCommonFriends(id, otherId); } -} +} \ No newline at end of file From 4a65a82561113b8d8a88d184fef1b9020f717283 Mon Sep 17 00:00:00 2001 From: Ksenia Date: Fri, 29 May 2026 22:25:15 +0300 Subject: [PATCH 05/29] feat: add ErrorHandler for centralized exception handling --- .../filmorate/exception/ErrorHandler.java | 35 +++++++++++++++++++ .../filmorate/service/UserService.java | 1 - 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 src/main/java/ru/yandex/practicum/filmorate/exception/ErrorHandler.java diff --git a/src/main/java/ru/yandex/practicum/filmorate/exception/ErrorHandler.java b/src/main/java/ru/yandex/practicum/filmorate/exception/ErrorHandler.java new file mode 100644 index 0000000..73a2102 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/exception/ErrorHandler.java @@ -0,0 +1,35 @@ +package ru.yandex.practicum.filmorate.exception; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.util.Map; + +@RestControllerAdvice +@Slf4j +public class ErrorHandler { + + @ExceptionHandler + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Map handleValidationException(ValidationException e) { + log.warn("400: {}", e.getMessage()); + return Map.of("error", e.getMessage()); + } + + @ExceptionHandler + @ResponseStatus(HttpStatus.NOT_FOUND) + public Map handleNotFoundException(NotFoundException e) { + log.warn("404: {}", e.getMessage()); + return Map.of("error", e.getMessage()); + } + + @ExceptionHandler + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public Map handleException(Exception e) { + log.error("500: {}", e.getMessage(), e); + return Map.of("error", e.getMessage()); + } +} \ 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 e1b2a04..2ba0a73 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java @@ -3,7 +3,6 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import ru.yandex.practicum.filmorate.exception.NotFoundException; import ru.yandex.practicum.filmorate.model.User; import ru.yandex.practicum.filmorate.storage.user.UserStorage; From d00b71ad79acd1a339a95ab8c66799ee0018a029 Mon Sep 17 00:00:00 2001 From: Ksenia Date: Fri, 29 May 2026 22:25:29 +0300 Subject: [PATCH 06/29] feat: add logbook-spring-boot-starter for HTTP logging --- pom.xml | 6 ++++++ src/main/resources/application.properties | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 091c1d5..d6903f7 100644 --- a/pom.xml +++ b/pom.xml @@ -44,6 +44,12 @@ spring-boot-starter-test test + + + org.zalando + logbook-spring-boot-starter + 3.7.2 + diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 8b13789..21af5c7 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1 @@ - +logging.level.org.zalando.logbook=TRACE From d3c66521753cb8ace743bddb06bff9581264b2e8 Mon Sep 17 00:00:00 2001 From: Ksenia Date: Fri, 29 May 2026 22:36:16 +0300 Subject: [PATCH 07/29] fix: handle MethodArgumentNotValidException with 400 status in ErrorHandler --- .../practicum/filmorate/exception/ErrorHandler.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/java/ru/yandex/practicum/filmorate/exception/ErrorHandler.java b/src/main/java/ru/yandex/practicum/filmorate/exception/ErrorHandler.java index 73a2102..a7128b6 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/exception/ErrorHandler.java +++ b/src/main/java/ru/yandex/practicum/filmorate/exception/ErrorHandler.java @@ -2,6 +2,7 @@ import lombok.extern.slf4j.Slf4j; 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; @@ -26,6 +27,14 @@ public Map handleNotFoundException(NotFoundException e) { return Map.of("error", e.getMessage()); } + @ExceptionHandler + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Map handleMethodArgumentNotValidException( + MethodArgumentNotValidException e) { + log.warn("400: {}", e.getMessage()); + return Map.of("error", e.getMessage()); + } + @ExceptionHandler @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public Map handleException(Exception e) { From 9bd72ee1b70d22805e9d8d356ae4919d1d751882 Mon Sep 17 00:00:00 2001 From: Ksenia Date: Fri, 29 May 2026 22:36:25 +0300 Subject: [PATCH 08/29] fix: restore release date validation in InMemoryFilmStorage --- .../storage/film/InMemoryFilmStorage.java | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java index b6206e0..391e98c 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java @@ -3,8 +3,10 @@ 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.Film; +import java.time.LocalDate; import java.util.Collection; import java.util.HashMap; import java.util.Map; @@ -13,13 +15,16 @@ @Slf4j public class InMemoryFilmStorage implements FilmStorage { + // Константа: дата первого релиза (день рождения кино) + private static final LocalDate CINEMA_BIRTHDAY = LocalDate.of(1895, 12, 28); + // Хранилище фильмов в памяти private final Map films = new HashMap<>(); - private long currentId = 1; @Override public Film add(Film film) { + validateReleaseDate(film); film.setId(currentId++); films.put(film.getId(), film); log.debug("Добавлен фильм: id={}, name={}", film.getId(), film.getName()); @@ -32,6 +37,7 @@ public Film update(Film film) { log.warn("Попытка обновить несуществующий фильм: id={}", film.getId()); throw new NotFoundException("Фильм с id=" + film.getId() + " не найден"); } + validateReleaseDate(film); films.put(film.getId(), film); log.debug("Обновлён фильм: id={}", film.getId()); return film; @@ -59,4 +65,14 @@ public Film findById(long id) { } return film; } + + // Валидация даты релиза — не может быть раньше дня рождения кино + private void validateReleaseDate(Film film) { + if (film.getReleaseDate().isBefore(CINEMA_BIRTHDAY)) { + log.warn("Некорректная дата релиза: {}", film.getReleaseDate()); + throw new ValidationException( + "Дата релиза не может быть раньше 28 декабря 1895 года" + ); + } + } } \ No newline at end of file From 140024dce30ed661a43dd7a63b6db0dd71a3e1ff Mon Sep 17 00:00:00 2001 From: Ksenia Date: Fri, 29 May 2026 22:54:18 +0300 Subject: [PATCH 09/29] test: update FilmControllerTest and UserControllerTest for new architecture --- .../filmorate/controller/FilmControllerTest.java | 10 ++++++++-- .../filmorate/controller/UserControllerTest.java | 8 ++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/test/java/ru/yandex/practicum/filmorate/controller/FilmControllerTest.java b/src/test/java/ru/yandex/practicum/filmorate/controller/FilmControllerTest.java index 032e3b4..2164488 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/controller/FilmControllerTest.java +++ b/src/test/java/ru/yandex/practicum/filmorate/controller/FilmControllerTest.java @@ -4,6 +4,9 @@ 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.service.FilmService; +import ru.yandex.practicum.filmorate.storage.film.InMemoryFilmStorage; +import ru.yandex.practicum.filmorate.storage.user.InMemoryUserStorage; import java.time.LocalDate; @@ -14,8 +17,11 @@ class FilmControllerTest { @BeforeEach void setUp() { - // Перед каждым тестом создаётся новый чистый контроллер - filmController = new FilmController(); + // Перед каждым тестом создаётся новая цепочка зависимостей + InMemoryFilmStorage filmStorage = new InMemoryFilmStorage(); + InMemoryUserStorage userStorage = new InMemoryUserStorage(); + FilmService filmService = new FilmService(filmStorage, userStorage); + filmController = new FilmController(filmService); } @Test diff --git a/src/test/java/ru/yandex/practicum/filmorate/controller/UserControllerTest.java b/src/test/java/ru/yandex/practicum/filmorate/controller/UserControllerTest.java index 3364305..5e41289 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/controller/UserControllerTest.java +++ b/src/test/java/ru/yandex/practicum/filmorate/controller/UserControllerTest.java @@ -4,6 +4,8 @@ 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.service.UserService; +import ru.yandex.practicum.filmorate.storage.user.InMemoryUserStorage; import java.time.LocalDate; @@ -15,8 +17,10 @@ class UserControllerTest { @BeforeEach void setUp() { - // Перед каждым тестом создаётся новый чистый контроллер - userController = new UserController(); + // Перед каждым тестом создаётся новая цепочка зависимостей + InMemoryUserStorage userStorage = new InMemoryUserStorage(); + UserService userService = new UserService(userStorage); + userController = new UserController(userService); } @Test From 35c1c625185dac9eb6b28415e85a7eb85687425e Mon Sep 17 00:00:00 2001 From: Ksenia Date: Fri, 29 May 2026 22:55:18 +0300 Subject: [PATCH 10/29] fix: restore missing validation calls in InMemoryUserStorage and InMemoryFilmStorage --- .../filmorate/storage/film/InMemoryFilmStorage.java | 2 +- .../filmorate/storage/user/InMemoryUserStorage.java | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java index 391e98c..7d2bead 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java @@ -71,7 +71,7 @@ private void validateReleaseDate(Film film) { if (film.getReleaseDate().isBefore(CINEMA_BIRTHDAY)) { log.warn("Некорректная дата релиза: {}", film.getReleaseDate()); throw new ValidationException( - "Дата релиза не может быть раньше 28 декабря 1895 года" + "Дата релиза фильма не может быть раньше 28 декабря 1895 года" ); } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java index d24167a..1afa2fb 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java @@ -3,6 +3,7 @@ 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.util.Collection; @@ -18,6 +19,7 @@ public class InMemoryUserStorage implements UserStorage { @Override public User add(User user) { + validateLogin(user); // Если имя пустое — используем логин (бизнес-правило из прошлого спринта) if (user.getName() == null || user.getName().isBlank()) { user.setName(user.getLogin()); @@ -34,6 +36,7 @@ public User update(User user) { log.warn("Попытка обновить несуществующего пользователя: id={}", user.getId()); throw new NotFoundException("Пользователь с id=" + user.getId() + " не найден"); } + validateLogin(user); if (user.getName() == null || user.getName().isBlank()) { user.setName(user.getLogin()); } @@ -64,4 +67,12 @@ public User findById(long id) { } return user; } + + // Логин не может содержать пробелы + private void validateLogin(User user) { + if (user.getLogin().contains(" ")) { + log.warn("Некорректный логин: '{}'", user.getLogin()); + throw new ValidationException("Логин не может содержать пробелы"); + } + } } \ No newline at end of file From ba21fd39bc1abbbddb37cb6f39b252cc34719933 Mon Sep 17 00:00:00 2001 From: Ksenia Date: Fri, 29 May 2026 22:56:57 +0300 Subject: [PATCH 11/29] docs: update README with sprint 2 architecture and new API endpoints --- README.md | 93 +++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 63 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index d74e832..ccf08e1 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,40 @@ # java-filmorate -Учебный backend-проект на Java и Spring Boot: сервис для работы с фильмами, оценками пользователей и возврата топ-5 фильмов, рекомендованных к просмотру. +Учебный backend-проект на Java и Spring Boot: сервис для работы с фильмами, +оценками пользователей и возврата топ фильмов, рекомендованных к просмотру. -Проект развивается поэтапно. На текущем этапе реализован базовый каркас Spring Boot приложения, REST API для управления пользователями и фильмами, in-memory хранилище и строгая валидация данных. +Проект развивается поэтапно. На текущем этапе реализованы слоистая архитектура +(controller → service → storage), социальные функции (друзья, лайки) +и полноценный REST API. ## Автор **Ксения Пылькина** *(Ksenia Pylkina)* ## Ключевые возможности на данном этапе -- CRUD-операции для `Film` и `User` (создание, обновление, получение списка). +- CRUD-операции для `Film` и `User` (создание, обновление, получение по ID и списком). - Хранение данных в оперативной памяти (`HashMap`). -- Автоматическая генерация уникальных идентификаторов. -- Строгая валидация входящих данных через `spring-boot-starter-validation` (`@Valid`, `@NotBlank`, `@Email`, `@Positive` и др.). -- Кастомная бизнес-валидация (проверка даты релиза кино, запрет пробелов в логине, автозамена пустого имени на логин). -- Обработка ошибок через кастомное исключение `ValidationException`. -- Логирование всех ключевых операций и ошибок с помощью `SLF4J`. -- Покрытие бизнес-логики и граничных условий unit-тестами. +- Социальные функции: добавление/удаление друзей, список общих друзей. +- Лайки фильмам: добавление/удаление, топ-N фильмов по количеству лайков. +- Слоистая архитектура: контроллер → сервис → хранилище. +- Внедрение зависимостей через конструктор (`@Autowired`). +- Строгая валидация входящих данных через `spring-boot-starter-validation`. +- Кастомная бизнес-валидация (дата релиза, пробелы в логине, автозамена имени на логин). +- Централизованная обработка ошибок (`@RestControllerAdvice`): 400, 404, 500. +- Логирование HTTP-запросов и ответов через Logbook. +- Покрытие бизнес-логики unit-тестами. ## Технологии - **Java 21** - **Spring Boot 3.2.2** - `spring-boot-starter-web` (REST API) - - `spring-boot-starter-validation` (Валидация данных) -- **Lombok** (Генерация boilerplate-кода и логгеров) -- **JUnit 5** (Unit-тестирование) -- **Maven** (Сборка проекта и управление зависимостями) -- **Checkstyle** (Контроль качества и стиля кода) + - `spring-boot-starter-validation` (валидация данных) +- **Logbook 3.7.2** (логирование HTTP-запросов) +- **Lombok** (генерация boilerplate-кода и логгеров) +- **JUnit 5** (unit-тестирование) +- **Maven** (сборка проекта) +- **Checkstyle** (контроль стиля кода) ## Запуск проекта @@ -38,40 +45,66 @@ После запуска сервер будет доступен на порту **8080**. -Для проверки HTTP-запросов можно использовать **Postman** (в корне проекта или в материалах курса доступна готовая коллекция тестов `postman.json`). +Для проверки запросов можно использовать **Postman** +(коллекция `postman.json` доступна в материалах курса). ## Основные HTTP-эндпоинты ### Фильмы (`/films`) - `GET /films` — получить список всех фильмов. +- `GET /films/{id}` — получить фильм по ID. - `POST /films` — добавить новый фильм. - `PUT /films` — обновить существующий фильм. +- `PUT /films/{id}/like/{userId}` — поставить лайк фильму. +- `DELETE /films/{id}/like/{userId}` — убрать лайк. +- `GET /films/popular?count={count}` — топ-N фильмов по лайкам (по умолчанию 10). ### Пользователи (`/users`) - `GET /users` — получить список всех пользователей. +- `GET /users/{id}` — получить пользователя по ID. - `POST /users` — создать нового пользователя. -- `PUT /users` — обновить данные существующего пользователя. +- `PUT /users` — обновить данные пользователя. +- `PUT /users/{id}/friends/{friendId}` — добавить в друзья. +- `DELETE /users/{id}/friends/{friendId}` — удалить из друзей. +- `GET /users/{id}/friends` — список друзей пользователя. +- `GET /users/{id}/friends/common/{otherId}` — общие друзья с другим пользователем. ## Реализация по этапам -
+
Этап 1 — Каркас Spring Boot приложения и базовый REST API -На этом этапе был заложен фундамент будущего сервиса рекомендаций фильмов. Проект переведен на рельсы Spring Boot, реализованы базовые модели данных и контроллеры. +На этом этапе был заложен фундамент будущего сервиса. Проект переведён на Spring Boot, +реализованы базовые модели данных и контроллеры. **Что сделано:** -- Настроен `pom.xml`, подключены необходимые стартеры Spring Boot, Lombok и плагин Checkstyle. -- Созданы модели данных `Film` и `User` с использованием аннотаций Lombok (`@Data`). +- Настроен `pom.xml`, подключены стартеры Spring Boot, Lombok и плагин Checkstyle. +- Созданы модели `Film` и `User` с аннотациями Lombok (`@Data`). - Реализованы REST-контроллеры `FilmController` и `UserController`. -- Настроено in-memory хранение данных с ручной генерацией ID. -- Добавлена валидация данных через аннотации `jakarta.validation.constraints`. -- Реализована сложная бизнес-валидация (например, дата релиза фильма не может быть раньше 28 декабря 1895 года). -- Настроено логирование запросов и ошибок. -- Написаны unit-тесты для проверки логики валидации контроллеров. - -**Ключевые решения:** -- Использование `spring-boot-starter-validation` позволило значительно сократить количество ручных проверок `if-else` в контроллерах и сделать код чище. -- Выделение кастомного исключения `ValidationException` подготовило почву для будущего создания глобального обработчика ошибок (`@ControllerAdvice`). -- Разделение логики валидации и сохранения данных внутри контроллеров делает код читаемым и легко тестируемым. +- In-memory хранение данных с ручной генерацией ID. +- Валидация через аннотации `jakarta.validation.constraints`. +- Кастомная бизнес-валидация (дата релиза, пробелы в логине). +- Логирование запросов и ошибок. +- Unit-тесты для проверки логики валидации. + +
+ +
+Этап 2 — Слоистая архитектура, социальные функции, полный REST + +На этом этапе логика вынесена из контроллеров, добавлены друзья и лайки, +API доведён до соответствия REST. + +**Что сделано:** +- Созданы интерфейсы `FilmStorage` и `UserStorage` и их реализации + `InMemoryFilmStorage` и `InMemoryUserStorage` (`@Component`). +- Созданы `FilmService` (`@Service`) и `UserService` (`@Service`) + с бизнес-логикой лайков и друзей. +- Контроллеры переработаны: зависят от сервисов, внедрение через конструктор. +- Сервисы зависят от интерфейсов хранилищ — легко заменить реализацию в будущем. +- Добавлены все новые эндпоинты (друзья, лайки, топ фильмов, получение по ID). +- Централизованная обработка ошибок через `@RestControllerAdvice`: 400, 404, 500. +- Подключён Logbook для логирования HTTP-запросов и ответов. +- Unit-тесты обновлены под новую архитектуру.
\ No newline at end of file From cffa019544ea10751273f6dcd02a0ed09dbe5b1b Mon Sep 17 00:00:00 2001 From: Ksenia Date: Fri, 29 May 2026 23:12:21 +0300 Subject: [PATCH 12/29] refactor: move validateReleaseDate from InMemoryFilmStorage to FilmService --- .../filmorate/service/FilmService.java | 26 ++++++++++++++++--- .../storage/film/InMemoryFilmStorage.java | 18 +------------ 2 files changed, 23 insertions(+), 21 deletions(-) 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 2e0e6d3..1c6b9ed 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java @@ -4,16 +4,21 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.exception.ValidationException; import ru.yandex.practicum.filmorate.model.Film; import ru.yandex.practicum.filmorate.storage.film.FilmStorage; import ru.yandex.practicum.filmorate.storage.user.UserStorage; +import java.time.LocalDate; import java.util.*; @Service @Slf4j public class FilmService { + // Константа: дата первого релиза (день рождения кино) + private static final LocalDate CINEMA_BIRTHDAY = LocalDate.of(1895, 12, 28); + private final FilmStorage filmStorage; // Нужен для проверки существования пользователя, который ставит лайк private final UserStorage userStorage; @@ -26,18 +31,18 @@ public FilmService(FilmStorage filmStorage, UserStorage userStorage) { public Film addLike(long filmId, long userId) { Film film = filmStorage.findById(filmId); - // Проверка существования пользователя — иначе 404 - userStorage.findById(userId); + userStorage.findById(userId); // Проверка существования пользователя — иначе 404 film.getLikes().add(userId); + + filmStorage.update(film); log.debug("Пользователь id={} поставил лайк фильму id={}", userId, filmId); return film; } public Film removeLike(long filmId, long userId) { Film film = filmStorage.findById(filmId); - // Проверка существования пользователя — иначе 404 - userStorage.findById(userId); + userStorage.findById(userId); // Проверка существования пользователя — иначе 404 if (!film.getLikes().contains(userId)) { throw new NotFoundException( @@ -46,6 +51,7 @@ public Film removeLike(long filmId, long userId) { } film.getLikes().remove(userId); + filmStorage.update(film); log.debug("Пользователь id={} убрал лайк с фильма id={}", userId, filmId); return film; } @@ -66,12 +72,24 @@ public int compare(Film f1, Film f2) { return allFilms.subList(0, resultSize); } + // Валидация даты релиза — не может быть раньше дня рождения кино + private void validateReleaseDate(Film film) { + if (film.getReleaseDate().isBefore(CINEMA_BIRTHDAY)) { + log.warn("Некорректная дата релиза: {}", film.getReleaseDate()); + throw new ValidationException( + "Дата релиза фильма не может быть раньше 28 декабря 1895 года" + ); + } + } + // Делегирование базовых операций хранилищу public Film add(Film film) { + validateReleaseDate(film); return filmStorage.add(film); } public Film update(Film film) { + validateReleaseDate(film); return filmStorage.update(film); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java index 7d2bead..9420d34 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java @@ -3,10 +3,8 @@ 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.Film; -import java.time.LocalDate; import java.util.Collection; import java.util.HashMap; import java.util.Map; @@ -15,16 +13,12 @@ @Slf4j public class InMemoryFilmStorage implements FilmStorage { - // Константа: дата первого релиза (день рождения кино) - private static final LocalDate CINEMA_BIRTHDAY = LocalDate.of(1895, 12, 28); - // Хранилище фильмов в памяти private final Map films = new HashMap<>(); private long currentId = 1; @Override public Film add(Film film) { - validateReleaseDate(film); film.setId(currentId++); films.put(film.getId(), film); log.debug("Добавлен фильм: id={}, name={}", film.getId(), film.getName()); @@ -37,7 +31,7 @@ public Film update(Film film) { log.warn("Попытка обновить несуществующий фильм: id={}", film.getId()); throw new NotFoundException("Фильм с id=" + film.getId() + " не найден"); } - validateReleaseDate(film); + films.put(film.getId(), film); log.debug("Обновлён фильм: id={}", film.getId()); return film; @@ -65,14 +59,4 @@ public Film findById(long id) { } return film; } - - // Валидация даты релиза — не может быть раньше дня рождения кино - private void validateReleaseDate(Film film) { - if (film.getReleaseDate().isBefore(CINEMA_BIRTHDAY)) { - log.warn("Некорректная дата релиза: {}", film.getReleaseDate()); - throw new ValidationException( - "Дата релиза фильма не может быть раньше 28 декабря 1895 года" - ); - } - } } \ No newline at end of file From 5f2989b845821b0b72892588e973c35e034ef6db Mon Sep 17 00:00:00 2001 From: Ksenia Date: Fri, 29 May 2026 23:12:31 +0300 Subject: [PATCH 13/29] refactor: move validateLogin from InMemoryUserStorage to UserService --- .../filmorate/service/UserService.java | 17 +++++++++++++++++ .../storage/user/InMemoryUserStorage.java | 12 +----------- 2 files changed, 18 insertions(+), 11 deletions(-) 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 2ba0a73..3e0ec59 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java @@ -3,6 +3,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.exception.ValidationException; import ru.yandex.practicum.filmorate.model.User; import ru.yandex.practicum.filmorate.storage.user.UserStorage; @@ -28,6 +29,9 @@ public User addFriend(long userId, long friendId) { user.getFriends().add(friendId); friend.getFriends().add(userId); + userStorage.update(user); + userStorage.update(friend); + log.debug("Пользователи id={} и id={} теперь друзья", userId, friendId); return user; } @@ -39,6 +43,9 @@ public User removeFriend(long userId, long friendId) { user.getFriends().remove(friendId); friend.getFriends().remove(userId); + userStorage.update(user); + userStorage.update(friend); + log.debug("Пользователи id={} и id={} больше не друзья", userId, friendId); return user; } @@ -69,12 +76,22 @@ public Collection getCommonFriends(long userId, long otherId) { return common; } + // Логин не может содержать пробелы + private void validateLogin(User user) { + if (user.getLogin().contains(" ")) { + log.warn("Некорректный логин: '{}'", user.getLogin()); + throw new ValidationException("Логин не может содержать пробелы"); + } + } + // Делегирование базовых операций хранилищу public User add(User user) { + validateLogin(user); return userStorage.add(user); } public User update(User user) { + validateLogin(user); return userStorage.update(user); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java index 1afa2fb..7f80cfa 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java @@ -3,7 +3,6 @@ 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.util.Collection; @@ -19,7 +18,6 @@ public class InMemoryUserStorage implements UserStorage { @Override public User add(User user) { - validateLogin(user); // Если имя пустое — используем логин (бизнес-правило из прошлого спринта) if (user.getName() == null || user.getName().isBlank()) { user.setName(user.getLogin()); @@ -36,7 +34,7 @@ public User update(User user) { log.warn("Попытка обновить несуществующего пользователя: id={}", user.getId()); throw new NotFoundException("Пользователь с id=" + user.getId() + " не найден"); } - validateLogin(user); + if (user.getName() == null || user.getName().isBlank()) { user.setName(user.getLogin()); } @@ -67,12 +65,4 @@ public User findById(long id) { } return user; } - - // Логин не может содержать пробелы - private void validateLogin(User user) { - if (user.getLogin().contains(" ")) { - log.warn("Некорректный логин: '{}'", user.getLogin()); - throw new ValidationException("Логин не может содержать пробелы"); - } - } } \ No newline at end of file From 30926fbe3f2267b745b8f61dd9d53a5abe9e2473 Mon Sep 17 00:00:00 2001 From: Ksenia Date: Fri, 29 May 2026 23:15:41 +0300 Subject: [PATCH 14/29] test: add FilmServiceTest and UserServiceTest with business logic coverage --- .../filmorate/service/FilmServiceTest.java | 123 ++++++++++++++++++ .../filmorate/service/UserServiceTest.java | 106 +++++++++++++++ 2 files changed, 229 insertions(+) create mode 100644 src/test/java/ru/yandex/practicum/filmorate/service/FilmServiceTest.java create mode 100644 src/test/java/ru/yandex/practicum/filmorate/service/UserServiceTest.java diff --git a/src/test/java/ru/yandex/practicum/filmorate/service/FilmServiceTest.java b/src/test/java/ru/yandex/practicum/filmorate/service/FilmServiceTest.java new file mode 100644 index 0000000..8797c4f --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/service/FilmServiceTest.java @@ -0,0 +1,123 @@ +package ru.yandex.practicum.filmorate.service; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.exception.ValidationException; +import ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.model.User; +import ru.yandex.practicum.filmorate.storage.film.InMemoryFilmStorage; +import ru.yandex.practicum.filmorate.storage.user.InMemoryUserStorage; + +import java.time.LocalDate; +import java.util.Collection; + +import static org.junit.jupiter.api.Assertions.*; + +class FilmServiceTest { + + private FilmService filmService; + private InMemoryUserStorage userStorage; + + @BeforeEach + void setUp() { + InMemoryFilmStorage filmStorage = new InMemoryFilmStorage(); + userStorage = new InMemoryUserStorage(); + filmService = new FilmService(filmStorage, userStorage); + } + + // Вспомогательные методы - отмена дублирования кода создания объектов в каждом тесте + private Film createFilm(String name) { + Film film = new Film(); + film.setName(name); + film.setDescription("Описание"); + film.setReleaseDate(LocalDate.of(2000, 1, 1)); + film.setDuration(120); + return filmService.add(film); + } + + private User createUser(String login) { + User user = new User(); + user.setEmail(login + "@mail.ru"); + user.setLogin(login); + user.setName(login); + user.setBirthday(LocalDate.of(1990, 1, 1)); + return userStorage.add(user); + } + + @Test + void shouldAddFilm() { + Film film = createFilm("Inception"); + assertNotNull(film.getId()); + } + + @Test + void shouldThrowWhenReleaseDateTooOld() { + Film film = new Film(); + film.setName("Old Movie"); + film.setDescription("Очень старый"); + film.setReleaseDate(LocalDate.of(1895, 12, 27)); + film.setDuration(100); + + assertThrows(ValidationException.class, () -> filmService.add(film)); + } + + @Test + void shouldAddAndRemoveLike() { + Film film = createFilm("Inception"); + User user = createUser("userOne"); + + filmService.addLike(film.getId(), user.getId()); + assertEquals(1, filmService.findById(film.getId()).getLikes().size()); + + filmService.removeLike(film.getId(), user.getId()); + assertEquals(0, filmService.findById(film.getId()).getLikes().size()); + } + + @Test + void shouldThrowWhenAddLikeFromNonExistentUser() { + Film film = createFilm("Inception"); + assertThrows(NotFoundException.class, () -> filmService.addLike(film.getId(), 999L)); + } + + @Test + void shouldThrowWhenRemoveLikeNotExist() { + Film film = createFilm("Inception"); + User user = createUser("userOne"); + + assertThrows(NotFoundException.class, + () -> filmService.removeLike(film.getId(), user.getId())); + } + + @Test + void shouldReturnPopularFilms() { + Film film1 = createFilm("Film1"); + Film film2 = createFilm("Film2"); + Film film3 = createFilm("Film3"); + + User user1 = createUser("user1"); + User user2 = createUser("user2"); + + // film2 получает 2 лайка, film1 — 1, film3 — 0 + filmService.addLike(film2.getId(), user1.getId()); + filmService.addLike(film2.getId(), user2.getId()); + filmService.addLike(film1.getId(), user1.getId()); + + Collection popular = filmService.getPopular(3); + Film[] films = popular.toArray(new Film[0]); + + assertEquals(film2.getId(), films[0].getId()); + assertEquals(film1.getId(), films[1].getId()); + assertEquals(film3.getId(), films[2].getId()); + } + + @Test + void shouldReturnPopularWithCountLimit() { + createFilm("Film1"); + createFilm("Film2"); + createFilm("Film3"); + + Collection popular = filmService.getPopular(2); + assertEquals(2, popular.size()); + } +} \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/service/UserServiceTest.java b/src/test/java/ru/yandex/practicum/filmorate/service/UserServiceTest.java new file mode 100644 index 0000000..c1fc21b --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/service/UserServiceTest.java @@ -0,0 +1,106 @@ +package ru.yandex.practicum.filmorate.service; + +import org.junit.jupiter.api.BeforeEach; +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.user.InMemoryUserStorage; + +import java.time.LocalDate; +import java.util.Collection; + +import static org.junit.jupiter.api.Assertions.*; + +class UserServiceTest { + + private UserService userService; + + @BeforeEach + void setUp() { + userService = new UserService(new InMemoryUserStorage()); + } + + private User createUser(String login) { + User user = new User(); + user.setEmail(login + "@mail.ru"); + user.setLogin(login); + user.setName(login); + user.setBirthday(LocalDate.of(1990, 1, 1)); + return userService.add(user); + } + + @Test + void shouldAddUser() { + User user = createUser("testLogin"); + assertNotNull(user.getId()); + } + + @Test + void shouldThrowWhenLoginContainsSpace() { + User user = new User(); + user.setEmail("test@mail.ru"); + user.setLogin("bad login"); + user.setName("Test"); + user.setBirthday(LocalDate.of(1990, 1, 1)); + + assertThrows(ValidationException.class, () -> userService.add(user)); + } + + @Test + void shouldReplaceEmptyNameWithLogin() { + User user = new User(); + user.setEmail("test@mail.ru"); + user.setLogin("testLogin"); + user.setName(""); + user.setBirthday(LocalDate.of(1990, 1, 1)); + + User saved = userService.add(user); + assertEquals("testLogin", saved.getName()); + } + + @Test + void shouldAddAndRemoveFriend() { + User user1 = createUser("user1"); + User user2 = createUser("user2"); + + userService.addFriend(user1.getId(), user2.getId()); + + // Оба должны видеть друг друга + assertTrue(userService.findById(user1.getId()).getFriends().contains(user2.getId())); + assertTrue(userService.findById(user2.getId()).getFriends().contains(user1.getId())); + + userService.removeFriend(user1.getId(), user2.getId()); + + assertFalse(userService.findById(user1.getId()).getFriends().contains(user2.getId())); + assertFalse(userService.findById(user2.getId()).getFriends().contains(user1.getId())); + } + + @Test + void shouldReturnCommonFriends() { + User user1 = createUser("user1"); + User user2 = createUser("user2"); + User common = createUser("common"); + + userService.addFriend(user1.getId(), common.getId()); + userService.addFriend(user2.getId(), common.getId()); + + Collection commonFriends = userService.getCommonFriends( + user1.getId(), user2.getId() + ); + + assertEquals(1, commonFriends.size()); + assertEquals(common.getId(), commonFriends.iterator().next().getId()); + } + + @Test + void shouldReturnEmptyCommonFriendsWhenNone() { + User user1 = createUser("user1"); + User user2 = createUser("user2"); + + Collection commonFriends = userService.getCommonFriends( + user1.getId(), user2.getId() + ); + + assertTrue(commonFriends.isEmpty()); + } +} \ No newline at end of file From ab390dda35155776439a3a72ba80153be13b8844 Mon Sep 17 00:00:00 2001 From: Ksenia Date: Fri, 29 May 2026 23:17:01 +0300 Subject: [PATCH 15/29] style: replace wildcard import with explicit imports in FilmService --- .../ru/yandex/practicum/filmorate/service/FilmService.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 1c6b9ed..08d3940 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java @@ -10,7 +10,11 @@ import ru.yandex.practicum.filmorate.storage.user.UserStorage; import java.time.LocalDate; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; @Service @Slf4j From 80e5f419da4520e6b9f969bc4c4180744e3606af Mon Sep 17 00:00:00 2001 From: Ksenia Date: Fri, 29 May 2026 23:32:01 +0300 Subject: [PATCH 16/29] refactor: encapsulate likes mutation in Film model methods --- .../java/ru/yandex/practicum/filmorate/model/Film.java | 8 ++++++++ .../yandex/practicum/filmorate/service/FilmService.java | 5 ++--- 2 files changed, 10 insertions(+), 3 deletions(-) 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 de7bc71..97f7679 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/Film.java +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Film.java @@ -31,4 +31,12 @@ public class Film { private Integer duration; private Set likes = new HashSet<>(); + + public void addLike(Long userId) { + likes.add(userId); + } + + public boolean removeLike(Long userId) { + return likes.remove(userId); + } } 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 08d3940..e5046ce 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java @@ -37,7 +37,7 @@ public Film addLike(long filmId, long userId) { Film film = filmStorage.findById(filmId); userStorage.findById(userId); // Проверка существования пользователя — иначе 404 - film.getLikes().add(userId); + film.addLike(userId); filmStorage.update(film); log.debug("Пользователь id={} поставил лайк фильму id={}", userId, filmId); @@ -48,13 +48,12 @@ public Film removeLike(long filmId, long userId) { Film film = filmStorage.findById(filmId); userStorage.findById(userId); // Проверка существования пользователя — иначе 404 - if (!film.getLikes().contains(userId)) { + if (!film.removeLike(userId)) { throw new NotFoundException( "Пользователь id=" + userId + " не ставил лайк фильму id=" + filmId ); } - film.getLikes().remove(userId); filmStorage.update(film); log.debug("Пользователь id={} убрал лайк с фильма id={}", userId, filmId); return film; From f6da9800b85e97650ee830655852c504a74ae627 Mon Sep 17 00:00:00 2001 From: Ksenia Date: Fri, 29 May 2026 23:32:10 +0300 Subject: [PATCH 17/29] refactor: encapsulate friends mutation in User model methods --- .../yandex/practicum/filmorate/model/User.java | 8 ++++++++ .../filmorate/service/UserService.java | 17 +++++++++++++---- 2 files changed, 21 insertions(+), 4 deletions(-) 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 8b35be3..11d905c 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/User.java +++ b/src/main/java/ru/yandex/practicum/filmorate/model/User.java @@ -28,4 +28,12 @@ public class User { private LocalDate birthday; private Set friends = new HashSet<>(); + + public void addFriend(Long friendId) { + friends.add(friendId); + } + + public boolean removeFriend(Long friendId) { + return friends.remove(friendId); + } } 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 3e0ec59..7fc48ad 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java @@ -29,8 +29,8 @@ public User addFriend(long userId, long friendId) { user.getFriends().add(friendId); friend.getFriends().add(userId); - userStorage.update(user); - userStorage.update(friend); + user.addFriend(friendId); + friend.addFriend(userId); log.debug("Пользователи id={} и id={} теперь друзья", userId, friendId); return user; @@ -43,8 +43,8 @@ public User removeFriend(long userId, long friendId) { user.getFriends().remove(friendId); friend.getFriends().remove(userId); - userStorage.update(user); - userStorage.update(friend); + user.removeFriend(friendId); + friend.removeFriend(userId); log.debug("Пользователи id={} и id={} больше не друзья", userId, friendId); return user; @@ -84,14 +84,23 @@ private void validateLogin(User user) { } } + private void applyDefaultName(User user) { + if (user.getName() == null || user.getName().isBlank()) { + user.setName(user.getLogin()); + log.debug("Имя пользователя не задано — используется логин: '{}'", user.getLogin()); + } + } + // Делегирование базовых операций хранилищу public User add(User user) { validateLogin(user); + applyDefaultName(user); return userStorage.add(user); } public User update(User user) { validateLogin(user); + applyDefaultName(user); return userStorage.update(user); } From d175b9652dafe77e2d94bfbe54c783cbeeb14d4b Mon Sep 17 00:00:00 2001 From: Ksenia Date: Fri, 29 May 2026 23:32:22 +0300 Subject: [PATCH 18/29] refactor: move name-blank default logic from storage to UserService --- .../filmorate/storage/user/InMemoryUserStorage.java | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java index 7f80cfa..4f98485 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java @@ -18,10 +18,6 @@ public class InMemoryUserStorage implements UserStorage { @Override public User add(User user) { - // Если имя пустое — используем логин (бизнес-правило из прошлого спринта) - if (user.getName() == null || user.getName().isBlank()) { - user.setName(user.getLogin()); - } user.setId(currentId++); users.put(user.getId(), user); log.debug("Добавлен пользователь: id={}, login={}", user.getId(), user.getLogin()); @@ -35,9 +31,6 @@ public User update(User user) { throw new NotFoundException("Пользователь с id=" + user.getId() + " не найден"); } - if (user.getName() == null || user.getName().isBlank()) { - user.setName(user.getLogin()); - } users.put(user.getId(), user); log.debug("Обновлён пользователь: id={}", user.getId()); return user; From 86849cfca34fdabea725cfa48a4560f17183aa6b Mon Sep 17 00:00:00 2001 From: Ksenia Date: Fri, 29 May 2026 23:38:03 +0300 Subject: [PATCH 19/29] fix: use UserService to create users in FilmServiceTest --- .../practicum/filmorate/service/UserService.java | 3 --- .../practicum/filmorate/service/FilmServiceTest.java | 10 +++++----- 2 files changed, 5 insertions(+), 8 deletions(-) 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 7fc48ad..c265b8d 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java @@ -40,9 +40,6 @@ public User removeFriend(long userId, long friendId) { User user = userStorage.findById(userId); User friend = userStorage.findById(friendId); - user.getFriends().remove(friendId); - friend.getFriends().remove(userId); - user.removeFriend(friendId); friend.removeFriend(userId); diff --git a/src/test/java/ru/yandex/practicum/filmorate/service/FilmServiceTest.java b/src/test/java/ru/yandex/practicum/filmorate/service/FilmServiceTest.java index 8797c4f..1a157d0 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/service/FilmServiceTest.java +++ b/src/test/java/ru/yandex/practicum/filmorate/service/FilmServiceTest.java @@ -17,13 +17,13 @@ class FilmServiceTest { private FilmService filmService; - private InMemoryUserStorage userStorage; + private UserService userService; @BeforeEach void setUp() { - InMemoryFilmStorage filmStorage = new InMemoryFilmStorage(); - userStorage = new InMemoryUserStorage(); - filmService = new FilmService(filmStorage, userStorage); + InMemoryUserStorage userStorage = new InMemoryUserStorage(); + userService = new UserService(userStorage); + filmService = new FilmService(new InMemoryFilmStorage(), userStorage); } // Вспомогательные методы - отмена дублирования кода создания объектов в каждом тесте @@ -42,7 +42,7 @@ private User createUser(String login) { user.setLogin(login); user.setName(login); user.setBirthday(LocalDate.of(1990, 1, 1)); - return userStorage.add(user); + return userService.add(user); } @Test From 05ebd50c16bcdfaec47aa3b3f43dc906adda9c9c Mon Sep 17 00:00:00 2001 From: Ksenia Date: Sat, 30 May 2026 13:21:07 +0300 Subject: [PATCH 20/29] fix: add count validation in FilmService.getPopular --- .../ru/yandex/practicum/filmorate/service/FilmService.java | 7 +++++++ 1 file changed, 7 insertions(+) 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 e5046ce..ee67d93 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java @@ -60,6 +60,13 @@ public Film removeLike(long filmId, long userId) { } public Collection getPopular(int count) { + // count должен быть положительным числом + if (count <= 0) { + throw new ValidationException( + "Количество фильмов должно быть положительным числом, получено: " + count + ); + } + List allFilms = new ArrayList<>(filmStorage.findAll()); // Сортировка по убыванию количества лайков From 8c83aa92c2c1563c729aa207cb7216d03ce4850f Mon Sep 17 00:00:00 2001 From: Ksenia Date: Sat, 30 May 2026 13:24:52 +0300 Subject: [PATCH 21/29] feat: add findPopular method to FilmStorage with stream-based sorting --- .../filmorate/storage/film/InMemoryFilmStorage.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java index 9420d34..f3906a1 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java @@ -7,6 +7,7 @@ import java.util.Collection; import java.util.HashMap; +import java.util.List; import java.util.Map; @Component @@ -59,4 +60,13 @@ public Film findById(long id) { } return film; } + + @Override + public List findPopular(int count) { + // Сортировка по убыванию лайков, берём первые count штук + return films.values().stream() + .sorted((f1, f2) -> f2.getLikes().size() - f1.getLikes().size()) + .limit(count) + .collect(java.util.stream.Collectors.toList()); + } } \ No newline at end of file From dbcde930964d8aa3c9847d5f3c68e3e1e169c8de Mon Sep 17 00:00:00 2001 From: Ksenia Date: Sat, 30 May 2026 13:25:11 +0300 Subject: [PATCH 22/29] fix: delegate sorting to storage in FilmService --- .../practicum/filmorate/service/FilmService.java | 14 +------------- .../filmorate/storage/film/FilmStorage.java | 3 +++ 2 files changed, 4 insertions(+), 13 deletions(-) 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 ee67d93..ccd47f3 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java @@ -67,19 +67,7 @@ public Collection getPopular(int count) { ); } - List allFilms = new ArrayList<>(filmStorage.findAll()); - - // Сортировка по убыванию количества лайков - Collections.sort(allFilms, new Comparator() { - @Override - public int compare(Film f1, Film f2) { - return f2.getLikes().size() - f1.getLikes().size(); - } - }); - - // Возвращение не больше count фильмов - int resultSize = Math.min(count, allFilms.size()); - return allFilms.subList(0, resultSize); + return filmStorage.findPopular(count); } // Валидация даты релиза — не может быть раньше дня рождения кино diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmStorage.java index 40e56d2..b8f2995 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmStorage.java @@ -3,6 +3,7 @@ import ru.yandex.practicum.filmorate.model.Film; import java.util.Collection; +import java.util.List; public interface FilmStorage { @@ -15,4 +16,6 @@ public interface FilmStorage { Collection findAll(); Film findById(long id); + + List findPopular(int count); } \ No newline at end of file From 1a883f8c6227c810900afd2f402b86870b8f0427 Mon Sep 17 00:00:00 2001 From: Ksenia Date: Sat, 30 May 2026 13:25:21 +0300 Subject: [PATCH 23/29] test: add test for invalid count in FilmService.getPopular --- .../yandex/practicum/filmorate/service/FilmServiceTest.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/test/java/ru/yandex/practicum/filmorate/service/FilmServiceTest.java b/src/test/java/ru/yandex/practicum/filmorate/service/FilmServiceTest.java index 1a157d0..d953f85 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/service/FilmServiceTest.java +++ b/src/test/java/ru/yandex/practicum/filmorate/service/FilmServiceTest.java @@ -120,4 +120,10 @@ void shouldReturnPopularWithCountLimit() { Collection popular = filmService.getPopular(2); assertEquals(2, popular.size()); } + + @Test + void shouldThrowWhenCountIsZeroOrNegative() { + assertThrows(ValidationException.class, () -> filmService.getPopular(0)); + assertThrows(ValidationException.class, () -> filmService.getPopular(-1)); + } } \ No newline at end of file From bdc674f4360020f391e6331687b9c577704d5d3d Mon Sep 17 00:00:00 2001 From: Ksenia Date: Sat, 30 May 2026 13:30:58 +0300 Subject: [PATCH 24/29] fix: add self-friendship validation in UserService friend methods and remove duplicate friends.add calls in UserService.addFriend --- .../filmorate/service/UserService.java | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) 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 c265b8d..07e9cf0 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java @@ -23,12 +23,11 @@ public UserService(UserStorage userStorage) { } public User addFriend(long userId, long friendId) { + validateDifferentUsers(userId, friendId); + User user = userStorage.findById(userId); User friend = userStorage.findById(friendId); - user.getFriends().add(friendId); - friend.getFriends().add(userId); - user.addFriend(friendId); friend.addFriend(userId); @@ -37,6 +36,8 @@ public User addFriend(long userId, long friendId) { } public User removeFriend(long userId, long friendId) { + validateDifferentUsers(userId, friendId); + User user = userStorage.findById(userId); User friend = userStorage.findById(friendId); @@ -59,6 +60,8 @@ public Collection getFriends(long userId) { } public Collection getCommonFriends(long userId, long otherId) { + validateDifferentUsers(userId, otherId); + User user = userStorage.findById(userId); User other = userStorage.findById(otherId); List common = new ArrayList<>(); @@ -88,6 +91,14 @@ private void applyDefaultName(User user) { } } + private void validateDifferentUsers(long userId, long otherId) { + if (userId == otherId) { + throw new ValidationException( + "Идентификаторы пользователей должны различаться, получено: id=" + userId + ); + } + } + // Делегирование базовых операций хранилищу public User add(User user) { validateLogin(user); From f99ba91ececd5ea0cac72be99999a69ae01effaa Mon Sep 17 00:00:00 2001 From: Ksenia Date: Sat, 30 May 2026 13:31:07 +0300 Subject: [PATCH 25/29] test: add tests for self-friendship validation in UserService --- .../filmorate/service/UserServiceTest.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/test/java/ru/yandex/practicum/filmorate/service/UserServiceTest.java b/src/test/java/ru/yandex/practicum/filmorate/service/UserServiceTest.java index c1fc21b..210d61e 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/service/UserServiceTest.java +++ b/src/test/java/ru/yandex/practicum/filmorate/service/UserServiceTest.java @@ -103,4 +103,25 @@ void shouldReturnEmptyCommonFriendsWhenNone() { assertTrue(commonFriends.isEmpty()); } + + @Test + void shouldThrowWhenAddFriendToYourself() { + User user = createUser("user1"); + assertThrows(ValidationException.class, + () -> userService.addFriend(user.getId(), user.getId())); + } + + @Test + void shouldThrowWhenRemoveFriendYourself() { + User user = createUser("user1"); + assertThrows(ValidationException.class, + () -> userService.removeFriend(user.getId(), user.getId())); + } + + @Test + void shouldThrowWhenGetCommonFriendsWithYourself() { + User user = createUser("user1"); + assertThrows(ValidationException.class, + () -> userService.getCommonFriends(user.getId(), user.getId())); + } } \ No newline at end of file From cc8359962abdd302d703b697ca0c734d8d583235 Mon Sep 17 00:00:00 2001 From: Ksenia Date: Sat, 30 May 2026 13:52:20 +0300 Subject: [PATCH 26/29] refactor: extract getFilmOrThrow and getUserOrThrow helper methods in services and move existence check from storage update to service layer --- .../filmorate/service/FilmService.java | 32 +++++++------ .../filmorate/service/UserService.java | 46 +++++++++++-------- .../filmorate/storage/film/FilmStorage.java | 3 +- .../storage/film/InMemoryFilmStorage.java | 14 ++---- .../storage/user/InMemoryUserStorage.java | 14 ++---- .../filmorate/storage/user/UserStorage.java | 3 +- 6 files changed, 56 insertions(+), 56 deletions(-) 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 ccd47f3..c1ee020 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java @@ -10,11 +10,7 @@ import ru.yandex.practicum.filmorate.storage.user.UserStorage; import java.time.LocalDate; -import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; @Service @Slf4j @@ -33,20 +29,31 @@ public FilmService(FilmStorage filmStorage, UserStorage userStorage) { this.userStorage = userStorage; } + // Получение фильма по id с проверкой существования + private Film getFilmOrThrow(long id) { + return filmStorage.findById(id) + .orElseThrow(() -> new NotFoundException("Фильм с id=" + id + " не найден")); + } + + // Получение пользователя по id с проверкой существования + private void getUserOrThrow(long id) { + userStorage.findById(id) + .orElseThrow(() -> new NotFoundException("Пользователь с id=" + id + " не найден")); + } + public Film addLike(long filmId, long userId) { - Film film = filmStorage.findById(filmId); - userStorage.findById(userId); // Проверка существования пользователя — иначе 404 + Film film = getFilmOrThrow(filmId); + getUserOrThrow(userId); // проверка существования пользователя — иначе 404 film.addLike(userId); - filmStorage.update(film); log.debug("Пользователь id={} поставил лайк фильму id={}", userId, filmId); return film; } public Film removeLike(long filmId, long userId) { - Film film = filmStorage.findById(filmId); - userStorage.findById(userId); // Проверка существования пользователя — иначе 404 + Film film = getFilmOrThrow(filmId); + getUserOrThrow(userId); // проверка существования пользователя — иначе 404 if (!film.removeLike(userId)) { throw new NotFoundException( @@ -60,13 +67,12 @@ public Film removeLike(long filmId, long userId) { } public Collection getPopular(int count) { - // count должен быть положительным числом + // Валидация: count должен быть положительным числом if (count <= 0) { throw new ValidationException( "Количество фильмов должно быть положительным числом, получено: " + count ); } - return filmStorage.findPopular(count); } @@ -80,13 +86,13 @@ private void validateReleaseDate(Film film) { } } - // Делегирование базовых операций хранилищу public Film add(Film film) { validateReleaseDate(film); return filmStorage.add(film); } public Film update(Film film) { + getFilmOrThrow(film.getId()); // проверка существования — иначе 404 validateReleaseDate(film); return filmStorage.update(film); } @@ -96,6 +102,6 @@ public Collection findAll() { } public Film findById(long id) { - return filmStorage.findById(id); + return getFilmOrThrow(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 07e9cf0..fb95354 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java @@ -3,6 +3,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +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.user.UserStorage; @@ -22,11 +23,25 @@ public UserService(UserStorage userStorage) { this.userStorage = userStorage; } + private void validateDifferentUsers(long userId, long otherId) { + if (userId == otherId) { + throw new ValidationException( + "Идентификаторы пользователей должны различаться, получено: id=" + userId + ); + } + } + + // Получение пользователя по id с проверкой существования + private User getUserOrThrow(long id) { + return userStorage.findById(id) + .orElseThrow(() -> new NotFoundException("Пользователь с id=" + id + " не найден")); + } + public User addFriend(long userId, long friendId) { validateDifferentUsers(userId, friendId); - User user = userStorage.findById(userId); - User friend = userStorage.findById(friendId); + User user = getUserOrThrow(userId); + User friend = getUserOrThrow(friendId); user.addFriend(friendId); friend.addFriend(userId); @@ -38,8 +53,8 @@ public User addFriend(long userId, long friendId) { public User removeFriend(long userId, long friendId) { validateDifferentUsers(userId, friendId); - User user = userStorage.findById(userId); - User friend = userStorage.findById(friendId); + User user = getUserOrThrow(userId); + User friend = getUserOrThrow(friendId); user.removeFriend(friendId); friend.removeFriend(userId); @@ -49,11 +64,11 @@ public User removeFriend(long userId, long friendId) { } public Collection getFriends(long userId) { - User user = userStorage.findById(userId); + User user = getUserOrThrow(userId); List friends = new ArrayList<>(); for (Long friendId : user.getFriends()) { - friends.add(userStorage.findById(friendId)); + friends.add(getUserOrThrow(friendId)); } return friends; @@ -62,14 +77,13 @@ public Collection getFriends(long userId) { public Collection getCommonFriends(long userId, long otherId) { validateDifferentUsers(userId, otherId); - User user = userStorage.findById(userId); - User other = userStorage.findById(otherId); + User user = getUserOrThrow(userId); + User other = getUserOrThrow(otherId); List common = new ArrayList<>(); for (Long friendId : user.getFriends()) { - // Если этот друг есть и у другого пользователя = общий if (other.getFriends().contains(friendId)) { - common.add(userStorage.findById(friendId)); + common.add(getUserOrThrow(friendId)); } } @@ -91,14 +105,6 @@ private void applyDefaultName(User user) { } } - private void validateDifferentUsers(long userId, long otherId) { - if (userId == otherId) { - throw new ValidationException( - "Идентификаторы пользователей должны различаться, получено: id=" + userId - ); - } - } - // Делегирование базовых операций хранилищу public User add(User user) { validateLogin(user); @@ -107,6 +113,8 @@ public User add(User user) { } public User update(User user) { + getUserOrThrow(user.getId()); + validateLogin(user); applyDefaultName(user); return userStorage.update(user); @@ -117,6 +125,6 @@ public Collection findAll() { } public User findById(long id) { - return userStorage.findById(id); + return getUserOrThrow(id); } } \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmStorage.java index b8f2995..2d5eff2 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmStorage.java @@ -4,6 +4,7 @@ import java.util.Collection; import java.util.List; +import java.util.Optional; public interface FilmStorage { @@ -15,7 +16,7 @@ public interface FilmStorage { Collection findAll(); - Film findById(long id); + Optional findById(long id); List findPopular(int count); } \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java index f3906a1..2ed518e 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java @@ -5,10 +5,7 @@ import ru.yandex.practicum.filmorate.exception.NotFoundException; import ru.yandex.practicum.filmorate.model.Film; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; @Component @Slf4j @@ -52,13 +49,8 @@ public Collection findAll() { } @Override - public Film findById(long id) { - Film film = films.get(id); - if (film == null) { - log.warn("Фильм не найден: id={}", id); - throw new NotFoundException("Фильм с id=" + id + " не найден"); - } - return film; + public Optional findById(long id) { + return Optional.ofNullable(films.get(id)); } @Override diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java index 4f98485..36029f6 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java @@ -8,6 +8,7 @@ import java.util.Collection; import java.util.HashMap; import java.util.Map; +import java.util.Optional; @Component @Slf4j @@ -26,10 +27,6 @@ public User add(User user) { @Override public User update(User user) { - if (!users.containsKey(user.getId())) { - log.warn("Попытка обновить несуществующего пользователя: id={}", user.getId()); - throw new NotFoundException("Пользователь с id=" + user.getId() + " не найден"); - } users.put(user.getId(), user); log.debug("Обновлён пользователь: id={}", user.getId()); @@ -50,12 +47,7 @@ public Collection findAll() { } @Override - public User findById(long id) { - User user = users.get(id); - if (user == null) { - log.warn("Пользователь не найден: id={}", id); - throw new NotFoundException("Пользователь с id=" + id + " не найден"); - } - return user; + public Optional findById(long id) { + return Optional.ofNullable(users.get(id)); } } \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserStorage.java index 1412132..5c2c298 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserStorage.java @@ -3,6 +3,7 @@ import ru.yandex.practicum.filmorate.model.User; import java.util.Collection; +import java.util.Optional; public interface UserStorage { @@ -14,5 +15,5 @@ public interface UserStorage { Collection findAll(); - User findById(long id); + Optional findById(long id); } \ No newline at end of file From 1c63dd8ac65f31fa634bd974dec0d47cd6f2b4e6 Mon Sep 17 00:00:00 2001 From: Ksenia Date: Sat, 30 May 2026 13:59:34 +0300 Subject: [PATCH 27/29] fix: handle Optional in delete methods and remove existence check from InMemoryFilmStorage.update --- .../filmorate/storage/film/InMemoryFilmStorage.java | 8 +++----- .../filmorate/storage/user/InMemoryUserStorage.java | 5 +++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java index 2ed518e..e2eb4ae 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java @@ -25,10 +25,6 @@ public Film add(Film film) { @Override public Film update(Film film) { - if (!films.containsKey(film.getId())) { - log.warn("Попытка обновить несуществующий фильм: id={}", film.getId()); - throw new NotFoundException("Фильм с id=" + film.getId() + " не найден"); - } films.put(film.getId(), film); log.debug("Обновлён фильм: id={}", film.getId()); @@ -37,7 +33,9 @@ public Film update(Film film) { @Override public Film delete(long id) { - Film film = findById(id); + Film film = findById(id) + .orElseThrow(() -> new NotFoundException("Фильм с id=" + id + " не найден")); + films.remove(id); log.debug("Удалён фильм: id={}", id); return film; diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java index 36029f6..1d20cba 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java @@ -35,12 +35,13 @@ public User update(User user) { @Override public User delete(long id) { - User user = findById(id); + User user = findById(id) + .orElseThrow(() -> new NotFoundException("Пользователь с id=" + id + " не найден")); + users.remove(id); log.debug("Удалён пользователь: id={}", id); return user; } - @Override public Collection findAll() { return users.values(); From 4c572b99201919af43a955087cc74c7e273f217e Mon Sep 17 00:00:00 2001 From: Ksenia Date: Sat, 30 May 2026 14:02:44 +0300 Subject: [PATCH 28/29] style: replace Collectors.toList() with Stream.toList() in findPopular --- .../filmorate/storage/film/InMemoryFilmStorage.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java index e2eb4ae..9d9dc97 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java @@ -5,7 +5,11 @@ import ru.yandex.practicum.filmorate.exception.NotFoundException; import ru.yandex.practicum.filmorate.model.Film; -import java.util.*; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; @Component @Slf4j @@ -57,6 +61,6 @@ public List findPopular(int count) { return films.values().stream() .sorted((f1, f2) -> f2.getLikes().size() - f1.getLikes().size()) .limit(count) - .collect(java.util.stream.Collectors.toList()); + .toList(); } } \ No newline at end of file From 6eb62716f1aafb32981c4f89f4b94530dc05f1dd Mon Sep 17 00:00:00 2001 From: Ksenia Date: Sat, 30 May 2026 14:04:17 +0300 Subject: [PATCH 29/29] style: replace Collectors.toList() with Stream.toList() in findPopular --- .../practicum/filmorate/storage/user/InMemoryUserStorage.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java index 1d20cba..e9106a5 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java @@ -42,6 +42,7 @@ public User delete(long id) { log.debug("Удалён пользователь: id={}", id); return user; } + @Override public Collection findAll() { return users.values();