diff --git a/pom.xml b/pom.xml index 00a0703..9c95881 100644 --- a/pom.xml +++ b/pom.xml @@ -38,6 +38,13 @@ org.springframework.boot spring-boot-starter-validation + + + org.zalando + logbook-spring-boot-starter + 3.7.2 + + 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 f51bdc0..068d66f 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java @@ -1,69 +1,121 @@ package ru.yandex.practicum.filmorate.controller; -import org.springframework.web.bind.annotation.RestController; - +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; import jakarta.validation.Valid; +import jakarta.validation.constraints.Positive; import ru.yandex.practicum.filmorate.exception.ValidationException; -import lombok.extern.slf4j.Slf4j; +import ru.yandex.practicum.filmorate.exception.NotFoundException; import ru.yandex.practicum.filmorate.model.Film; -import org.springframework.web.bind.annotation.*; +import ru.yandex.practicum.filmorate.service.FilmService; +import ru.yandex.practicum.filmorate.service.UserService; +import ru.yandex.practicum.filmorate.storage.InMemoryFilmStorage; +import ru.yandex.practicum.filmorate.storage.InMemoryUserStorage; import java.time.LocalDate; -import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; @RestController @RequestMapping("/films") @Slf4j +@Validated public class FilmController { - private final Map films = new HashMap<>(); - private Long idCounter = 1L; + private final FilmService filmService; + private final UserService userService; - private void validateFilm(Film film) { - if (film.getName() == null || film.getName().isBlank()) { - log.warn("Название фильма не может быть пустым"); - throw new ValidationException("Название фильма не может быть пустым"); - } - if (film.getDescription() != null && film.getDescription().length() > 200) { - log.warn("Максимальная длина описания — 200 символов"); - throw new ValidationException("Максимальная длина описания — 200 символов"); - } - if (film.getReleaseDate() == null || film.getReleaseDate().isBefore(LocalDate.of(1895, 12, 28))) { - log.warn("Дата релиза должна быть не раньше 28 декабря 1895 года"); - throw new ValidationException("Дата релиза должна быть не раньше 28 декабря 1895 года"); - } - if (film.getDuration() <= 0) { - log.warn("Продолжительность фильма должна быть положительным числом"); - throw new ValidationException("Продолжительность фильма должна быть положительным числом"); - } + public FilmController() { + this.filmService = new FilmService(new InMemoryFilmStorage()); + this.userService = new UserService(new InMemoryUserStorage()); + } + + @Autowired + public FilmController(FilmService filmService, UserService userService) { + this.filmService = filmService; + this.userService = userService; + } + + @GetMapping + public List getAllFilms() { + log.info("Fetching all films"); + return filmService.getAllFilms(); + } + + @GetMapping("/{id}") + public Film getFilm(@Positive @PathVariable Long id) { + log.info("Fetching film with id {}", id); + return filmService.getFilmById(id); } @PostMapping public Film addFilm(@Valid @RequestBody Film film) { + log.info("Adding film: {}", film); validateFilm(film); - film.setId(idCounter++); - films.put(film.getId(), film); - log.info("Добавлен фильм с ID {}: {}", film.getId(), film); - return film; + return filmService.addFilm(film); } @PutMapping public Film updateFilm(@Valid @RequestBody Film film) { - if (film.getId() == null || !films.containsKey(film.getId())) { - log.warn("Фильм с ID {} не найден", film.getId()); - throw new ValidationException("Фильм с указанным ID не существует"); - } + log.info("Updating film: {}", film); validateFilm(film); - films.put(film.getId(), film); - log.info("Обновлен фильм с ID {}: {}", film.getId(), film); - return film; + try { + return filmService.updateFilm(film); + } catch (NotFoundException e) { + log.warn("Film not found: {}", film.getId()); + throw new ValidationException("Film not found"); + } } - @GetMapping - public List getAllFilms() { - log.info("Получен запрос всех фильмов. Текущее количество: {}", films.size()); - return new ArrayList<>(films.values()); + @DeleteMapping("/{id}") + public void deleteFilm(@Positive @PathVariable Long id) { + log.info("Deleting film with id {}", id); + filmService.removeFilm(id); + } + + @PutMapping("/{id}/like/{userId}") + public void addLike(@Positive @PathVariable Long id, + @Positive @PathVariable Long userId) { + log.info("User {} likes film {}", userId, id); + userService.getUserById(userId); + filmService.addLike(id, userId); + } + + @DeleteMapping("/{id}/like/{userId}") + public void removeLike(@Positive @PathVariable Long id, + @Positive @PathVariable Long userId) { + log.info("User {} removes like from film {}", userId, id); + userService.getUserById(userId); + filmService.removeLike(id, userId); + } + + @GetMapping("/popular") + public List getPopularFilms(@RequestParam(defaultValue = "10") @Positive int count) { + log.info("Fetching top {} popular films", count); + return filmService.getPopularFilms(count); + } + + private void validateFilm(Film film) { + if (film == null) { + throw new ValidationException("Film cannot be null"); + } + if (film.getName() == null || film.getName().isBlank()) { + log.warn("Film name is invalid"); + throw new ValidationException("Film name is invalid"); + } + if (film.getDescription() != null && film.getDescription().length() > 200) { + log.warn("Film description is too long"); + throw new ValidationException("Description length exceeds 200 characters"); + } + if (film.getReleaseDate() == null || film.getReleaseDate().isBefore(LocalDate.of(1895, 12, 28))) { + log.warn("Film release date is invalid"); + throw new ValidationException("Release date is invalid"); + } + if (film.getDuration() <= 0) { + log.warn("Film duration is invalid"); + throw new ValidationException("Duration must be positive"); + } } } + + diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/GlobalExceptionHandler.java b/src/main/java/ru/yandex/practicum/filmorate/controller/GlobalExceptionHandler.java new file mode 100644 index 0000000..adc883f --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/GlobalExceptionHandler.java @@ -0,0 +1,57 @@ +package ru.yandex.practicum.filmorate.controller; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.validation.FieldError; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.exception.ValidationException; + +import java.util.Map; + +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity> handleMethodArgumentNotValid(MethodArgumentNotValidException ex) { + log.error("MethodArgumentNotValidException: {}", ex.getMessage(), ex); + String errorMsg = ex.getBindingResult() + .getFieldErrors() + .stream() + .findFirst() + .map(FieldError::getDefaultMessage) + .orElse("Validation failed"); + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(Map.of("error", errorMsg)); + } + + @ExceptionHandler(ValidationException.class) + public ResponseEntity> handleValidation(ValidationException ex) { + log.error("ValidationException: {}", ex.getMessage(), ex); + String msg = ex.getMessage(); + if (msg != null && msg.toLowerCase().contains("not found")) { + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(Map.of("error", msg)); + } + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(Map.of("error", msg)); + } + + @ExceptionHandler(NotFoundException.class) + public ResponseEntity> handleNotFound(NotFoundException ex) { + log.error("NotFoundException: {}", ex.getMessage(), ex); + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(Map.of("error", ex.getMessage())); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity> handleOther(Exception ex) { + log.error("Unexpected exception: {}", ex.getMessage(), ex); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(Map.of("error", "An unexpected error occurred")); + } +} 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 acff70f..15399cf 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java @@ -1,67 +1,119 @@ package ru.yandex.practicum.filmorate.controller; -import jakarta.validation.Valid; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Positive; import ru.yandex.practicum.filmorate.exception.ValidationException; +import ru.yandex.practicum.filmorate.exception.NotFoundException; import ru.yandex.practicum.filmorate.model.User; -import org.springframework.web.bind.annotation.*; +import ru.yandex.practicum.filmorate.service.UserService; +import ru.yandex.practicum.filmorate.storage.InMemoryUserStorage; import java.time.LocalDate; -import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; @RestController @RequestMapping("/users") @Slf4j +@Validated public class UserController { - private final Map users = new HashMap<>(); - private Long idCounter = 1L; + private final UserService userService; - private void validateUser(User user) { - if (user.getEmail() == null || user.getEmail().isBlank() || !user.getEmail().contains("@")) { - log.warn("Электронная почта не может быть пустой и должна содержать символ @"); - throw new ValidationException("Электронная почта не может быть пустой и должна содержать символ @"); - } - if (user.getLogin() == null || user.getLogin().isBlank() || user.getLogin().contains(" ")) { - log.warn("Логин не может быть пустым и содержать пробелы"); - throw new ValidationException("Логин не может быть пустым и содержать пробелы"); - } - if (user.getName() == null || user.getName().isBlank()) { - user.setName(user.getLogin()); - log.info("Для пользователя {} установлено имя из логина", user.getLogin()); - } - if (user.getBirthday() == null || user.getBirthday().isAfter(LocalDate.now())) { - log.warn("Дата рождения не может быть в будущем"); - throw new ValidationException("Дата рождения не может быть в будущем"); - } + public UserController() { + this.userService = new UserService(new InMemoryUserStorage()); + } + + @Autowired + public UserController(UserService userService) { + this.userService = userService; + } + + @GetMapping + public List getAllUsers() { + log.info("Fetching all users"); + return userService.getAllUsers(); + } + + @GetMapping("/{id}") + public User getUser(@Positive @PathVariable Long id) { + log.info("Fetching user with id {}", id); + return userService.getUserById(id); } @PostMapping public User createUser(@Valid @RequestBody User user) { + log.info("Creating user: {}", user); validateUser(user); - user.setId(idCounter++); - users.put(user.getId(), user); - log.info("Создан пользователь: {}", user); - return user; + return userService.createUser(user); } @PutMapping public User updateUser(@Valid @RequestBody User user) { - if (user.getId() == null || !users.containsKey(user.getId())) { - log.warn("Пользователь с id {} не найден", user.getId()); - throw new ValidationException("Пользователь с указанным id не существует"); - } + log.info("Updating user: {}", user); validateUser(user); - users.put(user.getId(), user); - log.info("Обновлен пользователь: {}", user); - return user; + try { + return userService.updateUser(user); + } catch (NotFoundException e) { + log.warn("User not found: {}", user.getId()); + throw new ValidationException("User not found"); + } } - @GetMapping - public List getAllUsers() { - log.info("Получен запрос всех пользователей. Текущее количество: {}", users.size()); - return new ArrayList<>(users.values()); + @DeleteMapping("/{id}") + public void deleteUser(@Positive @PathVariable Long id) { + log.info("Deleting user with id {}", id); + userService.removeUser(id); + } + + @PutMapping("/{id}/friends/{friendId}") + public void addFriend(@Positive @PathVariable Long id, + @Positive @PathVariable Long friendId) { + log.info("User {} adds friend {}", id, friendId); + userService.addFriend(id, friendId); + } + + @DeleteMapping("/{id}/friends/{friendId}") + public void removeFriend(@Positive @PathVariable Long id, + @Positive @PathVariable Long friendId) { + log.info("User {} removes friend {}", id, friendId); + userService.removeFriend(id, friendId); + } + + @GetMapping("/{id}/friends") + public List getFriends(@Positive @PathVariable Long id) { + log.info("Fetching friends of user {}", id); + return userService.getFriends(id); + } + + @GetMapping("/{id}/friends/common/{otherId}") + public List getCommonFriends(@Positive @PathVariable Long id, + @Positive @PathVariable Long otherId) { + log.info("Fetching common friends of users {} and {}", id, otherId); + return userService.getCommonFriends(id, otherId); + } + + private void validateUser(User user) { + if (user == null) { + throw new ValidationException("User cannot be null"); + } + if (user.getEmail() == null || user.getEmail().isBlank() || !user.getEmail().contains("@")) { + log.warn("Email is invalid"); + throw new ValidationException("Email is invalid"); + } + if (user.getLogin() == null || user.getLogin().isBlank() || user.getLogin().contains(" ")) { + log.warn("Login is invalid"); + throw new ValidationException("Login is invalid"); + } + if (user.getName() == null || user.getName().isBlank()) { + user.setName(user.getLogin()); + log.info("Name set to login for user {}", user.getLogin()); + } + if (user.getBirthday() == null || user.getBirthday().isAfter(LocalDate.now())) { + log.warn("Birthday is invalid"); + throw new ValidationException("Birthday is invalid"); + } } -} \ No newline at end of file +} 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..ad7129c --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/exception/NotFoundException.java @@ -0,0 +1,20 @@ +package ru.yandex.practicum.filmorate.exception; + + +public class NotFoundException extends RuntimeException { + public NotFoundException() { + super(); + } + + public NotFoundException(String message) { + super(message); + } + + public NotFoundException(String message, Throwable cause) { + super(message, cause); + } + + public NotFoundException(Throwable cause) { + super(cause); + } +} 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..ecea46f --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java @@ -0,0 +1,48 @@ +package ru.yandex.practicum.filmorate.service; + +import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.storage.FilmStorage; + +import java.util.List; + +@Service +public class FilmService { + private final FilmStorage filmStorage; + + public FilmService(FilmStorage filmStorage) { + this.filmStorage = filmStorage; + } + + public Film addFilm(Film film) { + return filmStorage.addFilm(film); + } + + public Film updateFilm(Film film) { + return filmStorage.updateFilm(film); + } + + public Film getFilmById(Long id) { + return filmStorage.getFilmById(id); + } + + public List getAllFilms() { + return filmStorage.getAllFilms(); + } + + public void removeFilm(Long id) { + filmStorage.removeFilm(id); + } + + public void addLike(Long filmId, Long userId) { + filmStorage.addLike(filmId, userId); + } + + public void removeLike(Long filmId, Long userId) { + filmStorage.removeLike(filmId, userId); + } + + public List getPopularFilms(int count) { + return filmStorage.getPopularFilms(count); + } +} 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..8046e56 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java @@ -0,0 +1,53 @@ +package ru.yandex.practicum.filmorate.service; + +import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.model.User; +import ru.yandex.practicum.filmorate.storage.UserStorage; + +import java.util.List; + +@Service +public class UserService { + private final UserStorage userStorage; + + public UserService(UserStorage userStorage) { + this.userStorage = userStorage; + } + + public User createUser(User user) { + return userStorage.addUser(user); + } + + public User updateUser(User user) { + return userStorage.updateUser(user); + } + + public User getUserById(Long id) { + return userStorage.getUserById(id); + } + + public List getAllUsers() { + return userStorage.getAllUsers(); + } + + public void removeUser(Long id) { + userStorage.removeUser(id); + } + + public void addFriend(Long userId, Long friendId) { + userStorage.addFriend(userId, friendId); + } + + public void removeFriend(Long userId, Long friendId) { + userStorage.removeFriend(userId, friendId); + } + + public List getFriends(Long userId) { + return userStorage.getFriends(userId); + } + + public List getCommonFriends(Long userId, Long otherId) { + return userStorage.getCommonFriends(userId, otherId); + } +} + diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/FilmStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/FilmStorage.java new file mode 100644 index 0000000..d557a87 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/FilmStorage.java @@ -0,0 +1,24 @@ +package ru.yandex.practicum.filmorate.storage; + +import ru.yandex.practicum.filmorate.model.Film; +import java.util.List; + +public interface FilmStorage { + + Film addFilm(Film film); + + Film updateFilm(Film film); + + Film getFilmById(Long id); + + List getAllFilms(); + + void removeFilm(Long id); + + void addLike(Long filmId, Long userId); + + void removeLike(Long filmId, Long userId); + + List getPopularFilms(int count); + +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/InMemoryFilmStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/InMemoryFilmStorage.java new file mode 100644 index 0000000..57f74b4 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/InMemoryFilmStorage.java @@ -0,0 +1,79 @@ +package ru.yandex.practicum.filmorate.storage; + +import org.springframework.stereotype.Component; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.model.Film; + +import java.util.*; + +@Component +public class InMemoryFilmStorage implements FilmStorage { + private final Map films = new HashMap<>(); + private final Map> likes = new HashMap<>(); + private long idCounter = 1L; + + @Override + public Film addFilm(Film film) { + film.setId(idCounter++); + films.put(film.getId(), film); + likes.put(film.getId(), new HashSet<>()); + return film; + } + + @Override + public Film updateFilm(Film film) { + if (film.getId() == null || !films.containsKey(film.getId())) { + throw new NotFoundException("Film not found"); + } + films.put(film.getId(), film); + return film; + } + + @Override + public Film getFilmById(Long id) { + Film film = films.get(id); + if (film == null) { + throw new NotFoundException("Film not found"); + } + return film; + } + + @Override + public List getAllFilms() { + return new ArrayList<>(films.values()); + } + + @Override + public void removeFilm(Long id) { + if (!films.containsKey(id)) { + throw new NotFoundException("Film not found"); + } + films.remove(id); + likes.remove(id); + } + + @Override + public void addLike(Long filmId, Long userId) { + if (!films.containsKey(filmId)) { + throw new NotFoundException("Film not found"); + } + likes.get(filmId).add(userId); + } + + @Override + public void removeLike(Long filmId, Long userId) { + if (!films.containsKey(filmId)) { + throw new NotFoundException("Film not found"); + } + likes.get(filmId).remove(userId); + } + + @Override + public List getPopularFilms(int count) { + return films.values().stream() + .sorted((f1, f2) -> Integer.compare( + likes.get(f2.getId()).size(), likes.get(f1.getId()).size())) + .limit(count) + .toList(); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/InMemoryUserStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/InMemoryUserStorage.java new file mode 100644 index 0000000..620aa36 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/InMemoryUserStorage.java @@ -0,0 +1,89 @@ +package ru.yandex.practicum.filmorate.storage; + +import org.springframework.stereotype.Component; +import ru.yandex.practicum.filmorate.model.User; +import ru.yandex.practicum.filmorate.exception.NotFoundException; + +import java.util.*; + +@Component +public class InMemoryUserStorage implements UserStorage { + private final Map users = new HashMap<>(); + private final Map> friends = new HashMap<>(); + private long idCounter = 1L; + + @Override + public User addUser(User user) { + user.setId(idCounter++); + users.put(user.getId(), user); + friends.put(user.getId(), new HashSet<>()); + return user; + } + + @Override + public User updateUser(User user) { + if (user.getId() == null || !users.containsKey(user.getId())) { + throw new NotFoundException("User not found"); + } + users.put(user.getId(), user); + return user; + } + + @Override + public User getUserById(Long id) { + User user = users.get(id); + if (user == null) { + throw new NotFoundException("User not found"); + } + return user; + } + + @Override + public List getAllUsers() { + return new ArrayList<>(users.values()); + } + + @Override + public void removeUser(Long id) { + if (!users.containsKey(id)) { + throw new NotFoundException("User not found"); + } + users.remove(id); + friends.remove(id); + } + + @Override + public void addFriend(Long userId, Long friendId) { + getUserById(userId); + getUserById(friendId); + friends.get(userId).add(friendId); + friends.get(friendId).add(userId); + } + + @Override + public void removeFriend(Long userId, Long friendId) { + getUserById(userId); + getUserById(friendId); + friends.get(userId).remove(friendId); + friends.get(friendId).remove(userId); + } + + @Override + public List getFriends(Long userId) { + getUserById(userId); + return friends.get(userId).stream() + .map(users::get) + .toList(); + } + + @Override + public List getCommonFriends(Long userId, Long otherId) { + getUserById(userId); + getUserById(otherId); + Set common = new HashSet<>(friends.get(userId)); + common.retainAll(friends.get(otherId)); + return common.stream() + .map(users::get) + .toList(); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/UserStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/UserStorage.java new file mode 100644 index 0000000..d230cf0 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/UserStorage.java @@ -0,0 +1,26 @@ +package ru.yandex.practicum.filmorate.storage; + +import ru.yandex.practicum.filmorate.model.User; +import java.util.List; + +public interface UserStorage { + + User addUser(User user); + + User updateUser(User user); + + User getUserById(Long id); + + List getAllUsers(); + + void removeUser(Long id); + + void addFriend(Long userId, Long friendId); + + void removeFriend(Long userId, Long friendId); + + List getFriends(Long userId); + + List getCommonFriends(Long userId, Long otherId); + +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 8b13789..1d1b4bf 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1 @@ - +logging.level.org.zalando.logbook: TRACE