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.
+
+## Схема базы данных
+
+
+
+### Таблицы
+
+- **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 extends Payload>[] 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