diff --git a/README.md b/README.md index 2cf454a..c35ae4c 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,16 @@ # java-filmorate -Template repository for Filmorate project. + +## Схема базы данных + +![Схема БД](/img/schema-database.png) + +### Таблицы + +- **users** — таблица пользователей: `email` - электронная почта, `login` - логин, `name` - имя, `birthday` - день рождения +- **films** — таблица фильмов: `name` - название, `description` - описание, `release_date` - дата релиза, `duration` - продолжительность в минутах, `rating_id` - идентификатор рейтинга +- **friendships** — таблица дружбы между пользователями: `status` - статус заявки (0 - заявка в друзья не принята, 1 - заявка в друзья принята) +- **film_likes** — таблица лайков (пользователь отмечает понравившийся фильм) +- **film_genres** — таблица связей между фильмами и жанрами +- **ratings** — таблица возрастных рейтингов фильма: `name` - название рейтинга +- **genres** — таблица жанров фильмов: `name` - название жанра + diff --git a/img/schema-database.png b/img/schema-database.png new file mode 100644 index 0000000..473408b Binary files /dev/null and b/img/schema-database.png differ diff --git a/pom.xml b/pom.xml index 0cad031..6ad9433 100644 --- a/pom.xml +++ b/pom.xml @@ -1,46 +1,63 @@ - 4.0.0 - - org.springframework.boot - spring-boot-starter-parent - 3.2.4 - - - ru.yandex.practicum - filmorate - 0.0.1-SNAPSHOT - filmorate - filmorate - - 21 - - - - org.springframework.boot - spring-boot-starter-web - + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.4 + + + ru.yandex.practicum + filmorate + 0.0.1-SNAPSHOT + filmorate + filmorate + + 21 + + + + org.springframework.boot + spring-boot-starter-web + - - org.projectlombok - lombok - provided - - - org.springframework.boot - spring-boot-starter-test - test - - + + org.projectlombok + lombok + provided + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-starter-validation + + + org.zalando + logbook-spring-boot-starter + 3.7.2 + + - - - - org.springframework.boot - spring-boot-maven-plugin - - - + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + -XX:+EnableDynamicAgentLoading + + + + diff --git a/src/main/java/ru/yandex/practicum/filmorate/FilmorateApplication.java b/src/main/java/ru/yandex/practicum/filmorate/FilmorateApplication.java index dca451b..2e4a618 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/FilmorateApplication.java +++ b/src/main/java/ru/yandex/practicum/filmorate/FilmorateApplication.java @@ -5,8 +5,7 @@ @SpringBootApplication public class FilmorateApplication { - public static void main(String[] args) { - SpringApplication.run(FilmorateApplication.class, args); - } - -} + public static void main(String[] args) { + SpringApplication.run(FilmorateApplication.class, args); + } +} \ No newline at end of file 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 08cf0a1..ed7be62 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java @@ -1,7 +1,52 @@ package ru.yandex.practicum.filmorate.controller; -import org.springframework.web.bind.annotation.RestController; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Positive; +import lombok.RequiredArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import ru.yandex.practicum.filmorate.service.FilmService; +import ru.yandex.practicum.filmorate.model.Film; + +import java.util.Collection; +import java.util.List; @RestController +@RequiredArgsConstructor +@RequestMapping("/films") public class FilmController { + + private final FilmService filmService; + + @GetMapping + public Collection getFilms() { + return filmService.getFilms(); + } + + @PostMapping + public Film createFilm(@Valid @RequestBody Film film) { + return filmService.createFilm(film); + } + + @PutMapping + public Film updateFilm(@Valid @RequestBody Film film) { + return filmService.updateFilm(film); + } + + @PutMapping("/{id}/like/{userId}") + public void addLike(@PathVariable Long id, @PathVariable Long userId) { + filmService.addLike(id, userId); + } + + @DeleteMapping("/{id}/like/{userId}") + public void removeLike(@PathVariable Long id, @PathVariable Long userId) { + filmService.removeLike(id, userId); + } + + @Validated + @GetMapping("/popular") + public List getPopularFilms(@Positive @RequestParam(defaultValue = "10") int count) { + return filmService.getPopularFilms(count); + } + } diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java new file mode 100644 index 0000000..189c38e --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java @@ -0,0 +1,54 @@ +package ru.yandex.practicum.filmorate.controller; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import ru.yandex.practicum.filmorate.service.UserService; +import ru.yandex.practicum.filmorate.model.User; + +import java.util.Collection; +import java.util.Optional; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/users") +public class UserController { + + private final UserService userService; + + @GetMapping + public Collection getUsers() { + return userService.getUsers(); + } + + @PostMapping + public User createFilm(@Valid @RequestBody User user) { + return userService.createUser(user); + } + + @PutMapping + public User updateUser(@Valid @RequestBody User user) { + return userService.updateUser(user); + } + + @PutMapping("/{id}/friends/{friendId}") + public void addFriend(@PathVariable Long id, @PathVariable Long friendId) { + userService.addFriend(id, friendId); + } + + @DeleteMapping("/{id}/friends/{friendId}") + public void deleteFriend(@PathVariable Long id, @PathVariable Long friendId) { + userService.removeFriend(id, friendId); + } + + @GetMapping("/{id}/friends") + public Collection getFriends(@PathVariable Long id) { + return userService.getFriends(id); + } + + @GetMapping("/{id}/friends/common/{otherId}") + public Collection> getCommonFriends(@PathVariable Long id, @PathVariable Long otherId) { + return userService.getCommonFriends(id, otherId); + } + +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/exception/ApplicationExceptionHandler.java b/src/main/java/ru/yandex/practicum/filmorate/exception/ApplicationExceptionHandler.java new file mode 100644 index 0000000..5250509 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/exception/ApplicationExceptionHandler.java @@ -0,0 +1,32 @@ +package ru.yandex.practicum.filmorate.exception; + +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; + +@RestControllerAdvice +public class ApplicationExceptionHandler { + + @ExceptionHandler(MethodArgumentNotValidException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorResponse handleIncorrectParameter(final MethodArgumentNotValidException e) { + return new ErrorResponse( + "Ошибка валидации.", e.getMessage() + ); + } + + @ExceptionHandler(ExceptionNotFound.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public ErrorResponse handleObjectNotFound() { + return new ErrorResponse("Запрошенный объект не найден.", "Такой страницы не существует или запрошенный объект не найден."); + } + + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ErrorResponse handleException() { + return new ErrorResponse("Внутренняя ошибка сервера.", "Попробуйте обратиться позже."); + } + +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/exception/ErrorResponse.java b/src/main/java/ru/yandex/practicum/filmorate/exception/ErrorResponse.java new file mode 100644 index 0000000..e04a4ac --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/exception/ErrorResponse.java @@ -0,0 +1,15 @@ +package ru.yandex.practicum.filmorate.exception; + +import lombok.Getter; + +@Getter +public class ErrorResponse { + private final String error; + private final String description; + + public ErrorResponse(String error, String description) { + this.error = error; + this.description = description; + } + +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/exception/ExceptionNotFound.java b/src/main/java/ru/yandex/practicum/filmorate/exception/ExceptionNotFound.java new file mode 100644 index 0000000..0062a08 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/exception/ExceptionNotFound.java @@ -0,0 +1,7 @@ +package ru.yandex.practicum.filmorate.exception; + +public class ExceptionNotFound extends RuntimeException { + public ExceptionNotFound(String message) { + super(message); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/json/DeserializerDuration.java b/src/main/java/ru/yandex/practicum/filmorate/json/DeserializerDuration.java new file mode 100644 index 0000000..1bfaf1b --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/json/DeserializerDuration.java @@ -0,0 +1,19 @@ +package ru.yandex.practicum.filmorate.json; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; + +import java.io.IOException; +import java.time.Duration; + +public class DeserializerDuration extends JsonDeserializer { + @Override + public Duration deserialize(JsonParser jsonParser, DeserializationContext context) throws IOException { + if (jsonParser.getCurrentToken() == JsonToken.VALUE_NULL) { + return null; + } + return Duration.ofMinutes(jsonParser.getLongValue()); + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/json/SerializerDuration.java b/src/main/java/ru/yandex/practicum/filmorate/json/SerializerDuration.java new file mode 100644 index 0000000..bd984c3 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/json/SerializerDuration.java @@ -0,0 +1,19 @@ +package ru.yandex.practicum.filmorate.json; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import java.io.IOException; +import java.time.Duration; + +public class SerializerDuration extends JsonSerializer { + @Override + public void serialize(Duration value, JsonGenerator jsonGenerator, SerializerProvider serializers) throws IOException { + if (value == null) { + jsonGenerator.writeNull(); + } else { + jsonGenerator.writeNumber(value.toMinutes()); + } + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/Film.java b/src/main/java/ru/yandex/practicum/filmorate/model/Film.java index 3614a44..f5cf2c0 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/Film.java +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Film.java @@ -1,12 +1,34 @@ package ru.yandex.practicum.filmorate.model; -import lombok.Getter; -import lombok.Setter; - -/** - * Film. - */ -@Getter -@Setter +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import ru.yandex.practicum.filmorate.json.DeserializerDuration; +import ru.yandex.practicum.filmorate.json.SerializerDuration; +import ru.yandex.practicum.filmorate.validation.MinimumDate; +import ru.yandex.practicum.filmorate.validation.PositiveDuration; +import lombok.Data; + +import java.time.Duration; +import java.time.LocalDate; +import java.util.HashSet; +import java.util.Set; + +@Data public class Film { + + private Long id; + @NotBlank + private String name; + @Size(max = 200) + private String description; + @MinimumDate + private LocalDate releaseDate; + @PositiveDuration + @JsonSerialize(using = SerializerDuration.class) + @JsonDeserialize(using = DeserializerDuration.class) + private Duration duration; + private final 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 new file mode 100644 index 0000000..4e90863 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/User.java @@ -0,0 +1,28 @@ +package ru.yandex.practicum.filmorate.model; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.PastOrPresent; +import jakarta.validation.constraints.Pattern; +import lombok.Data; + +import java.time.LocalDate; +import java.util.HashSet; +import java.util.Set; + +@Data +public class User { + + private Long id; + @NotBlank + @Email + private String email; + @NotBlank + @Pattern(regexp = "(?=\\S+$).+", message = "Логин не должен содержать пробелы.") + private String login; + private String name; + @PastOrPresent + private LocalDate birthday; + private final Set friends = new HashSet<>(); + +} 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..8d54f38 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java @@ -0,0 +1,53 @@ +package ru.yandex.practicum.filmorate.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.exception.ExceptionNotFound; +import ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.storage.film.FilmStorage; + +import java.util.Collection; +import java.util.List; + +@RequiredArgsConstructor +@Service +public class FilmService { + + private final FilmStorage filmStorage; + private final UserService userService; + + public Collection getFilms() { + return filmStorage.getFilms(); + } + + public Film createFilm(Film film) { + return filmStorage.createFilm(film); + } + + public Film updateFilm(Film film) { + return filmStorage.updateFilm(film) + .orElseThrow(() -> new ExceptionNotFound("Фильм с идентификатором '%d' не найден".formatted(film.getId()))); + } + + public void addLike(Long filmId, Long userId) { + if (userService.getUserById(userId).getId() > 0 && getFilmById(filmId).getId() > 0) { + filmStorage.addLike(filmId, userId); + } + } + + public void removeLike(Long filmId, Long userId) { + if (userService.getUserById(userId).getId() > 0 && getFilmById(filmId).getId() > 0) { + filmStorage.removeLike(filmId, userId); + } + } + + public List getPopularFilms(int count) { + return filmStorage.getPopularFilms(count); + } + + public Film getFilmById(Long id) { + return filmStorage.getFilmById(id) + .orElseThrow(() -> new ExceptionNotFound("Фильм с идентификатором '%d' не найден".formatted(id))); + } + +} 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..3107cda --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java @@ -0,0 +1,60 @@ +package ru.yandex.practicum.filmorate.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.exception.ExceptionNotFound; +import ru.yandex.practicum.filmorate.model.User; +import ru.yandex.practicum.filmorate.storage.user.UserStorage; + +import java.util.Collection; +import java.util.Optional; +import java.util.Set; + +@RequiredArgsConstructor +@Service +public class UserService { + + private final UserStorage userStorage; + + public Collection getUsers() { + return userStorage.getUsers(); + } + + public User createUser(User user) { + return userStorage.createUser(user); + } + + public User updateUser(User user) { + return userStorage.updateUser(user) + .orElseThrow(() -> new ExceptionNotFound("Пользователь с идентификатором '%d' не найден".formatted(user.getId()))); + } + + public void addFriend(Long userId, Long friendId) { + if (getUserById(userId).getId() > 0 && getUserById(friendId).getId() > 0) { + userStorage.addFriend(userId, friendId); + } + } + + public void removeFriend(Long userId, Long friendId) { + if (getUserById(userId).getId() > 0 && getUserById(friendId).getId() > 0) { + userStorage.removeFriend(userId, friendId); + } + } + + public Set> getCommonFriends(Long userIdOne, Long userIdTwo) { + return userStorage.getCommonFriends(userIdOne, userIdTwo); + } + + public Set getFriends(Long userId) { + if (getUserById(userId).getId() > 0) { + return userStorage.getFriends(userId); + } + return null; + } + + public User getUserById(Long id) { + return userStorage.getUserById(id) + .orElseThrow(() -> new ExceptionNotFound("Пользователь с идентификатором '%d' не найден".formatted(id))); + } + +} 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..ef0e726 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmStorage.java @@ -0,0 +1,25 @@ +package ru.yandex.practicum.filmorate.storage.film; + +import ru.yandex.practicum.filmorate.model.Film; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +public interface FilmStorage { + + Collection getFilms(); + + Film createFilm(Film film); + + Optional updateFilm(Film film); + + void addLike(Long filmId, Long userId); + + Optional getFilmById(Long filmId); + + void removeLike(Long filmId, Long userId); + + List getPopularFilms(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 new file mode 100644 index 0000000..22950f9 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java @@ -0,0 +1,124 @@ +package ru.yandex.practicum.filmorate.storage.film; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import ru.yandex.practicum.filmorate.model.Film; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +@Slf4j +@Component +public class InMemoryFilmStorage implements FilmStorage { + + private final Map films = new HashMap<>(); + private Long id = 1L; + + @Override + public Collection getFilms() { + log.trace("Вызван метод getFilms"); + try { + log.trace("Успешно выполнен метод getFilms"); + return films.values(); + } catch (RuntimeException e) { + log.error("Ошибка в методе getFilms: {}", e.getMessage(), e); + throw e; + } + } + + @Override + public Film createFilm(Film film) { + log.trace("Вызван метод createFilm"); + try { + log.trace("Успешно выполнен метод createFilm"); + film.setId(generateId()); + films.put(film.getId(), film); + return film; + } catch (RuntimeException e) { + log.error("Ошибка в методе createFilm: {}", e.getMessage(), e); + throw e; + } + } + + @Override + public Optional updateFilm(Film film) { + log.trace("Вызван метод updateFilm"); + try { + log.trace("Успешно выполнен метод updateFilm"); + return Optional.ofNullable(films.computeIfPresent(film.getId(), (a, b) -> film)); + } catch (RuntimeException e) { + log.error("Ошибка в методе updateFilm: {}", e.getMessage(), e); + throw e; + } + } + + @Override + public void addLike(Long filmId, Long userId) { + log.trace("Вызван метод addLike"); + try { + log.trace("Успешно выполнен метод addLike"); + films.get(filmId).getLikes().add(userId); + } catch (RuntimeException e) { + log.error("Ошибка в методе addLike: {}", e.getMessage(), e); + throw e; + } + } + + @Override + public Optional getFilmById(Long id) { + log.trace("Вызван метод getFilmById"); + try { + log.trace("Успешно выполнен метод getFilmById"); + return Optional.ofNullable(films.get(id)); + } catch (RuntimeException e) { + log.error("Ошибка в методе getFilmById: {}", e.getMessage(), e); + throw e; + } + } + + @Override + public void removeLike(Long filmId, Long userId) { + log.trace("Вызван метод removeLike"); + try { + log.trace("Успешно выполнен метод removeLike"); + films.get(filmId).getLikes().remove(userId); + } catch (RuntimeException e) { + log.error("Ошибка в методе removeLike: {}", e.getMessage(), e); + throw e; + } + } + + @Override + public List getPopularFilms(int count) { + log.trace("Вызван метод getPopularFilms"); + try { + log.trace("Успешно выполнен метод getPopularFilms"); + return films.entrySet() + .stream() + .collect( + Collectors.toMap( + Map.Entry::getKey, + filmEntry -> filmEntry.getValue().getLikes().size() + ) + ) + .entrySet() + .stream() + .sorted(Map.Entry.comparingByValue().reversed()) + .limit(count) + .map(entry -> films.get(entry.getKey())) + .collect(Collectors.toList()); + } catch (RuntimeException e) { + log.error("Ошибка в методе getPopularFilms: {}", e.getMessage(), e); + throw e; + } + } + + private Long generateId() { + return id++; + } + +} \ 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..50e76bf --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java @@ -0,0 +1,132 @@ +package ru.yandex.practicum.filmorate.storage.user; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import ru.yandex.practicum.filmorate.model.User; + +import java.util.*; +import java.util.stream.Collectors; + +@Slf4j +@Component +public class InMemoryUserStorage implements UserStorage { + + private final Map users = new HashMap<>(); + private Long id = 1L; + + @Override + public Collection getUsers() { + log.trace("Вызван метод getUsers"); + try { + log.trace("Успешно выполнен метод getUsers"); + return users.values(); + } catch (RuntimeException e) { + log.error("Ошибка в методе getUsers: {}", e.getMessage(), e); + throw e; + } + } + + @Override + public User createUser(User user) { + log.trace("Вызван метод createUser"); + try { + if (user.getName() == null) { + user.setName(user.getLogin()); + log.debug("Имя пользователя не указано, поэтому используем логин как имя"); + } + user.setId(generateId()); + users.put(user.getId(), user); + log.trace("Успешно выполнен метод createUser"); + return user; + } catch (RuntimeException e) { + log.error("Ошибка в методе createUser: {}", e.getMessage(), e); + throw e; + } + } + + @Override + public Optional updateUser(User user) { + log.trace("Вызван метод updateUser"); + try { + log.trace("Успешно выполнен метод updateUser"); + return Optional.ofNullable(users.computeIfPresent(user.getId(), (a, b) -> user)); + } catch (RuntimeException e) { + log.error("Ошибка в методе updateUser: {}", e.getMessage(), e); + throw e; + } + } + + @Override + public void addFriend(Long userId, Long friendId) { + log.trace("Вызван метод addFriend"); + try { + log.trace("Успешно выполнен метод addFriend"); + users.get(userId).getFriends().add(friendId); + users.get(friendId).getFriends().add(userId); + } catch (RuntimeException e) { + log.error("Ошибка в методе addFriend: {}", e.getMessage(), e); + throw e; + } + } + + @Override + public Optional getUserById(Long id) { + log.trace("Вызван метод getUserById"); + try { + log.trace("Успешно выполнен метод getUserById"); + return Optional.ofNullable(users.get(id)); + } catch (RuntimeException e) { + log.error("Ошибка в методе getUserById: {}", e.getMessage(), e); + throw e; + } + } + + @Override + public void removeFriend(Long userId, Long friendId) { + log.trace("Вызван метод removeFriend"); + try { + log.trace("Успешно выполнен метод removeFriend"); + users.get(userId).getFriends().remove(friendId); + users.get(friendId).getFriends().remove(userId); + } catch (RuntimeException e) { + log.error("Ошибка в методе removeFriend: {}", e.getMessage(), e); + throw e; + } + } + + @Override + public Set> getCommonFriends(Long userIdOne, Long userIdTwo) { + log.trace("Вызван метод getCommonFriends"); + try { + log.trace("Успешно выполнен метод getCommonFriends"); + return users.get(userIdOne).getFriends() + .stream() + .filter(users.get(userIdTwo).getFriends()::contains) + .map(this::getUserById) + .collect(Collectors.toSet()); + } catch (RuntimeException e) { + log.error("Ошибка в методе getCommonFriends: {}", e.getMessage(), e); + throw e; + } + } + + @Override + public Set getFriends(Long userId) { + log.trace("Вызван метод getFriends"); + try { + log.trace("Успешно выполнен метод getFriends"); + return users.get(userId).getFriends() + .stream() + .map(users::get) + .collect(Collectors.toSet()); + } catch (RuntimeException e) { + log.error("Ошибка в методе getFriends: {}", e.getMessage(), e); + throw e; + } + } + + private Long generateId() { + return id++; + } + +} 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..e92e42d --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserStorage.java @@ -0,0 +1,29 @@ +package ru.yandex.practicum.filmorate.storage.user; + +import ru.yandex.practicum.filmorate.model.User; + +import java.util.Collection; +import java.util.Optional; +import java.util.Set; + +public interface UserStorage { + + Collection getUsers(); + + User createUser(User user); + + Optional updateUser(User user); + + void addFriend(Long userId, Long friendId); + + Optional getUserById(Long id); + + void removeFriend(Long userId, Long friendId); + + Set> getCommonFriends(Long userIdOne, Long userIdTwo); + + Set getFriends(Long ownerId); + +} + + diff --git a/src/main/java/ru/yandex/practicum/filmorate/validation/MinimumDate.java b/src/main/java/ru/yandex/practicum/filmorate/validation/MinimumDate.java new file mode 100644 index 0000000..d2d24f2 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/validation/MinimumDate.java @@ -0,0 +1,21 @@ +package ru.yandex.practicum.filmorate.validation; + +import jakarta.validation.Constraint; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +@Constraint(validatedBy = MinimumDateValidator.class) +public @interface MinimumDate { + String message() default "Дата релиза фильма не может быть раньше {value}"; + + Class[] groups() default {}; + + Class[] payload() default {}; + + String value() default "1895-12-28"; +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/validation/MinimumDateValidator.java b/src/main/java/ru/yandex/practicum/filmorate/validation/MinimumDateValidator.java new file mode 100644 index 0000000..b340a14 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/validation/MinimumDateValidator.java @@ -0,0 +1,20 @@ +package ru.yandex.practicum.filmorate.validation; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +import java.time.LocalDate; + +public class MinimumDateValidator implements ConstraintValidator { + private LocalDate minimumDate; + + @Override + public void initialize(MinimumDate constraintAnnotation) { + minimumDate = LocalDate.parse(constraintAnnotation.value()); + } + + @Override + public boolean isValid(LocalDate value, ConstraintValidatorContext context) { + return value == null || !value.isBefore(minimumDate); + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/validation/PositiveDuration.java b/src/main/java/ru/yandex/practicum/filmorate/validation/PositiveDuration.java new file mode 100644 index 0000000..8890be6 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/validation/PositiveDuration.java @@ -0,0 +1,20 @@ +package ru.yandex.practicum.filmorate.validation; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +@Constraint(validatedBy = PositiveDurationValidator.class) +public @interface PositiveDuration { + String message() default "Продолжительность фильма должна быть положительной"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/validation/PositiveDurationValidator.java b/src/main/java/ru/yandex/practicum/filmorate/validation/PositiveDurationValidator.java new file mode 100644 index 0000000..0a8851d --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/validation/PositiveDurationValidator.java @@ -0,0 +1,15 @@ +package ru.yandex.practicum.filmorate.validation; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +import java.time.Duration; + +public class PositiveDurationValidator implements + ConstraintValidator { + + @Override + public boolean isValid(Duration value, ConstraintValidatorContext context) { + return value == null || value.getSeconds() > 0; + } +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 8b13789..cc392b5 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1 @@ - +logging.level.org.zalando.logbook=TRACE \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/FilmorateApplicationTests.java b/src/test/java/ru/yandex/practicum/filmorate/FilmorateApplicationTests.java index 660412e..91559e2 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/FilmorateApplicationTests.java +++ b/src/test/java/ru/yandex/practicum/filmorate/FilmorateApplicationTests.java @@ -6,8 +6,8 @@ @SpringBootTest class FilmorateApplicationTests { - @Test - void contextLoads() { - } + @Test + void contextLoads() { + } -} +} \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/ValidationFilmTest.java b/src/test/java/ru/yandex/practicum/filmorate/ValidationFilmTest.java new file mode 100644 index 0000000..3f9c4a9 --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/ValidationFilmTest.java @@ -0,0 +1,139 @@ +package ru.yandex.practicum.filmorate; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.boot.test.context.SpringBootTest; +import ru.yandex.practicum.filmorate.model.Film; + +import java.time.Duration; +import java.time.LocalDate; +import java.util.Set; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +public class ValidationFilmTest { + + private static Validator validator; + private Film film; + + @BeforeAll + static void setupValidator() { + try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { + validator = factory.getValidator(); + } + } + + @BeforeEach + void setupValidFilm() { + film = new Film(); + film.setId(1L); + film.setName("Name Film"); + film.setDescription("Description Film"); + film.setReleaseDate(LocalDate.of(2025, 6, 1)); + film.setDuration(Duration.ofMinutes(120)); + } + + @Test + void validationNameDescriptionDateDurationFilm() { + Set> violations = validator.validate(film); + assertThat(violations).isEmpty(); + } + + + @ParameterizedTest + @MethodSource("validName") + void validationName(String name, boolean isValid) { + film.setName(name); + + boolean isViolation = validator.validate(film) + .stream() + .anyMatch(a -> a.getPropertyPath().toString().equals("name")); + + assertThat(isViolation).isEqualTo(!isValid); + } + + private static Stream validName() { + return Stream.of( + Arguments.of("Name", true), + Arguments.of("Name Film", true), + Arguments.of("Name-Film", true), + Arguments.of("Name_Film", true), + Arguments.of("Название фильма", true), + Arguments.of(" ", false), + Arguments.of(" ", false), + Arguments.of("", false), + Arguments.of(null, false) + ); + } + + @ParameterizedTest + @MethodSource("validDescription") + void validationDescriptionMax200Characters(String description, boolean isValid) { + film.setDescription(description); + + boolean isViolation = validator.validate(film) + .stream() + .anyMatch(a -> a.getPropertyPath().toString().equals("description")); + + assertThat(isViolation).isEqualTo(!isValid); + } + + private static Stream validDescription() { + return Stream.of( + Arguments.of("Name Film", true), + Arguments.of("Name Film ".repeat(20), true), + Arguments.of("Name Film ".repeat(20) + "!", false) + ); + } + + @ParameterizedTest + @MethodSource("validReleaseDate") + void validationReleaseDate(LocalDate date, boolean isValid) { + film.setReleaseDate(date); + + boolean isViolation = validator.validate(film) + .stream() + .anyMatch(a -> a.getPropertyPath().toString().equals("releaseDate")); + + assertThat(isViolation).isEqualTo(!isValid); + } + + private static Stream validReleaseDate() { + return Stream.of( + Arguments.of(LocalDate.of(1895, 12, 29), true), + Arguments.of(LocalDate.of(1895, 12, 28), true), + Arguments.of(LocalDate.of(1895, 12, 27), false) + ); + } + + @ParameterizedTest + @MethodSource("validDurations") + void validationDuration(Duration duration, boolean isValid) { + film.setDuration(duration); + + boolean isViolation = validator.validate(film) + .stream() + .anyMatch(a -> a.getPropertyPath().toString().equals("duration")); + + assertThat(isViolation).isEqualTo(!isValid); + } + + private static Stream validDurations() { + return Stream.of( + Arguments.of(Duration.ofMinutes(1), true), + Arguments.of(Duration.ZERO, false), + Arguments.of(Duration.ofMinutes(-1), false) + ); + } + +} \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/ValidationUserTest.java b/src/test/java/ru/yandex/practicum/filmorate/ValidationUserTest.java new file mode 100644 index 0000000..1820e19 --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/ValidationUserTest.java @@ -0,0 +1,119 @@ +package ru.yandex.practicum.filmorate; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.boot.test.context.SpringBootTest; +import ru.yandex.practicum.filmorate.model.User; + +import java.time.LocalDate; +import java.util.Set; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +public class ValidationUserTest { + + private static Validator validator; + private User user; + + + @BeforeAll + static void setupValidator() { + try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { + validator = factory.getValidator(); + } + } + + @BeforeEach + void setupValidUser() { + user = new User(); + user.setId(1L); + user.setEmail("internet-prodashi@yandex.ru"); + user.setLogin("internet-prodashi"); + user.setName("Ivan"); + user.setBirthday(LocalDate.of(1990, 2, 5)); + } + + @Test + void validationEmailLoginAndBirthdayUser() { + Set> violations = validator.validate(user); + assertThat(violations).isEmpty(); + } + + @ParameterizedTest + @MethodSource("validEmail") + void validationEmail(String email, boolean isValid) { + user.setEmail(email); + + boolean isViolation = validator.validate(user) + .stream() + .anyMatch(a -> a.getPropertyPath().toString().equals("email")); + + assertThat(isViolation).isEqualTo(!isValid); + } + + private static Stream validEmail() { + return Stream.of( + Arguments.of("internet-prodashi@yandex.ru", true), + Arguments.of(" ", false), + Arguments.of(" ", false), + Arguments.of("", false), + Arguments.of("internet prodashi@yandex.ru", false), + Arguments.of("internet-prodashiyandex.ru", false), + Arguments.of(null, false) + ); + } + + @ParameterizedTest + @MethodSource("validLogin") + void validationLogin(String login, boolean isValid) { + user.setLogin(login); + + boolean isViolation = validator.validate(user) + .stream() + .anyMatch(a -> a.getPropertyPath().toString().equals("login")); + + assertThat(isViolation).isEqualTo(!isValid); + } + + private static Stream validLogin() { + return Stream.of( + Arguments.of("internet-prodashi", true), + Arguments.of("internet prodashi", false), + Arguments.of(" ", false), + Arguments.of(" ", false), + Arguments.of("", false), + Arguments.of(null, false) + ); + } + + @ParameterizedTest + @MethodSource("validBirthday") + void validationBirthday(LocalDate birthday, boolean isValid) { + user.setBirthday(birthday); + + boolean isViolation = validator.validate(user) + .stream() + .anyMatch(a -> a.getPropertyPath().toString().equals("birthday")); + + assertThat(isViolation).isEqualTo(!isValid); + } + + private static Stream validBirthday() { + return Stream.of( + Arguments.of(LocalDate.of(1990, 2, 5), true), + Arguments.of(LocalDate.now(), true), + Arguments.of(LocalDate.now().plusDays(1), false) + ); + } + +} \ No newline at end of file