Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
3d442a2
fix: updated pom and config files
CrodiYa Nov 20, 2025
15639a2
feat: add sql files for db
CrodiYa Nov 20, 2025
53e6b2d
feat: Genre and Mpa model
CrodiYa Nov 20, 2025
6d9c86e
feat: Genre and Mpa service
CrodiYa Nov 20, 2025
73e0de6
feat: Genre and Mpa controller
CrodiYa Nov 20, 2025
fdf68a5
feat: add Genre and Mpa db storages
CrodiYa Nov 20, 2025
0fbd8a3
feat: add Film and Like db storages
CrodiYa Nov 20, 2025
07fad9c
feat: add User and FriendShip db storages
CrodiYa Nov 20, 2025
b32f90f
fix: deleted size() method
CrodiYa Nov 20, 2025
7f04be8
feat: add row mappers for models
CrodiYa Nov 20, 2025
b64b3c3
fix: moved in memory storages and refactored logic
CrodiYa Nov 20, 2025
20d593f
feat: add BaseDao to help dbStorages
CrodiYa Nov 20, 2025
d57054f
fix: updated services to work with db and inMemory
CrodiYa Nov 20, 2025
c640ee8
fix: add new field to Film
CrodiYa Nov 20, 2025
4dcacb4
fix: add DataIntegrityViolationException handler
CrodiYa Nov 20, 2025
db862b0
fix: add new methods to controllers
CrodiYa Nov 20, 2025
7feef8c
feat: add sql config files for testdb
CrodiYa Nov 20, 2025
98d2910
feat: add tests for db
CrodiYa Nov 20, 2025
7a8478a
fix: updated test for memory
CrodiYa Nov 20, 2025
0978476
fix: updated tests
CrodiYa Nov 20, 2025
04016c1
feat: add test for new services
CrodiYa Nov 20, 2025
9ae5d7f
feat: add test for new controllers
CrodiYa Nov 20, 2025
41136ef
fix: updated tests for controllers
CrodiYa Nov 20, 2025
b59e040
fix: checkstyle
CrodiYa Nov 20, 2025
ca57da2
feat: add db scheme
CrodiYa Nov 20, 2025
b33a5bc
fix: add field encapsulation
CrodiYa Nov 21, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added Filmorate.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@
Filmorate - это RESTful API для управления фильмами и пользователями.
Сейчас приложение позволяет добавлять, обновлять и просматривать информацию о фильмах и пользователях.

## Схема базы данных
(![Схема базы данных](Filmorate.png))

- #### films - хранит все фильмы
- #### users - хранит всех пользователей
- #### mpa - хранит MPA рейтинг
- #### genres - хранит жанры
- #### films_genres - связующая таблица между фильмами и жанрами
- #### likes - хранит лайки фильма
- #### friendships - хранит друзей пользователя

## Модели данных

### Film
Expand Down
10 changes: 10 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,16 @@
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>

</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,18 @@ public Collection<Film> getFilms() {
return filmService.getAllFilms();
}

/**
* Handles GET method.
* <p>Retrieves film from the storage.
*
* @param id film`s id. Must be positive number
* @return Film.
*/
@GetMapping("/{id}")
public Film getFilm(@PathVariable @Positive Long id) {
return filmService.getFilm(id);
}

/**
* Handles POST method.
* <p>Creates film in storage after validation.
Expand Down Expand Up @@ -70,6 +82,17 @@ public Film updateFilm(@Valid @RequestBody Film newFilm) {
return filmService.updateFilm(newFilm);
}

/**
* Handles DELETE method.
* <p>Deletes film from the storage.
*
* @param id film`s id. Must be positive number
*/
@DeleteMapping("/{id}")
public void deleteFilm(@PathVariable @Positive Long id) {
filmService.deleteFilm(id);
}

/**
* Handles GET method.
* <p>Retrieves top {@code count} popular films based on likes.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package ru.yandex.practicum.filmorate.controller;

import jakarta.validation.constraints.Positive;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import ru.yandex.practicum.filmorate.model.Genre;
import ru.yandex.practicum.filmorate.service.film.GenreService;

import java.util.Collection;

@RestController
@RequestMapping("/genres")
public class GenreController {

private final GenreService service;

public GenreController(GenreService service) {
this.service = service;
}

@GetMapping("/{id}")
public Genre getGenre(@PathVariable @Positive Long id) {
return service.getGenre(id);
}

@GetMapping
public Collection<Genre> getAll() {
return service.getAll();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package ru.yandex.practicum.filmorate.controller;

import jakarta.validation.constraints.Positive;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import ru.yandex.practicum.filmorate.model.Mpa;
import ru.yandex.practicum.filmorate.service.film.MpaService;

import java.util.Collection;

@RestController
@RequestMapping("/mpa")
public class MpaController {

private final MpaService service;

public MpaController(MpaService service) {
this.service = service;
}

@GetMapping("/{id}")
public Mpa getMpa(@PathVariable @Positive Long id) {
return service.getMpa(id);
}

@GetMapping
public Collection<Mpa> getAll() {
return service.getAll();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,18 @@ public Collection<User> getUsers() {
return userService.getAllUsers();
}

/**
* Handles GET method.
* <p>Retrieves user from the storage.
*
* @param id user`s id. Must be positive number
* @return User.
*/
@GetMapping("/{id}")
public User getUser(@PathVariable @Positive Long id) {
return userService.getUser(id);
}

/**
* Handles POST method.
* <p>Creates user in storage after validation.
Expand Down Expand Up @@ -67,6 +79,17 @@ public User updateUser(@Valid @RequestBody User newUser) {
return userService.updateUser(newUser);
}

/**
* Handles DELETE method.
* <p>Deletes user from the storage.
*
* @param id user`s id. Must be positive number
*/
@DeleteMapping("/{id}")
public void deleteUser(@PathVariable @Positive Long id) {
userService.deleteUser(id);
}

/**
* Handles GET method.
* <p>Return collection of user`s friends.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.ConstraintViolationException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
Expand Down Expand Up @@ -102,6 +103,33 @@ public ResponseEntity<ApiError> handleMismatchAndConstraintViolation(
return createBadRequest(request.getRequestURI(), Map.of("error", "Invalid request format"));
}

@ExceptionHandler(DataIntegrityViolationException.class)
public ResponseEntity<ApiError> handleDataIntegrityViolationException(
DataIntegrityViolationException ex,
HttpServletRequest request) {

logInfo(ex, "Handling DataIntegrityViolationException");
String msg = ex.getMessage().toLowerCase();
HttpStatus status = HttpStatus.BAD_REQUEST;
String errorMsg = "Invalid request format";

if (msg.contains("foreign key")) {

if (msg.contains("mpa")) {
errorMsg = "Mpa не найден";
status = HttpStatus.NOT_FOUND;
} else if (msg.contains("genre")) {
errorMsg = "Один из указанных жанров не найден";
status = HttpStatus.NOT_FOUND;
}
}

return createResponseEntity(
status,
request.getRequestURI(),
Map.of("error", errorMsg));
}

/**
* Helper for HttpMessageNotReadableException handler
* <p>Extracts meaningful error messages from JSON parsing exceptions.
Expand Down
6 changes: 6 additions & 0 deletions src/main/java/ru/yandex/practicum/filmorate/model/Film.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
import ru.yandex.practicum.filmorate.validation.ValidReleaseDate;

import java.time.LocalDate;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;


/**
Expand Down Expand Up @@ -54,4 +56,8 @@ public Film(Long id, String name, String description, LocalDate releaseDate, Int

@JsonProperty(access = JsonProperty.Access.READ_ONLY)
private Long likes = 0L;

private Mpa mpa;

private Set<Genre> genres = ConcurrentHashMap.newKeySet();
}
16 changes: 16 additions & 0 deletions src/main/java/ru/yandex/practicum/filmorate/model/Genre.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package ru.yandex.practicum.filmorate.model;

import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Genre {

@NotNull
private Long id;
private String name;
}
16 changes: 16 additions & 0 deletions src/main/java/ru/yandex/practicum/filmorate/model/Mpa.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package ru.yandex.practicum.filmorate.model;

import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Mpa {

@NotNull
private Long id;
private String name;
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
package ru.yandex.practicum.filmorate.service.film;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import ru.yandex.practicum.filmorate.exception.NotFoundException;
import ru.yandex.practicum.filmorate.exception.ValidationException;
import ru.yandex.practicum.filmorate.model.Film;
import ru.yandex.practicum.filmorate.storage.film.FilmStorage;
import ru.yandex.practicum.filmorate.storage.film.GenreStorage;
import ru.yandex.practicum.filmorate.storage.film.LikeStorage;
import ru.yandex.practicum.filmorate.storage.user.UserStorage;

import java.util.Collection;
import java.util.Comparator;

/**
* Main service for film operations and business logic.
Expand All @@ -22,19 +24,25 @@
* @see FilmStorage
*/
@Service
@Transactional
public class FilmService implements FilmServiceInterface {

private final FilmStorage filmStorage;
private final UserStorage userStorage;
private final LikeStorage likeStorage;
private final GenreStorage genreStorage;

/**
* Constructor for dependency injection
*/
public FilmService(FilmStorage filmStorage, UserStorage userStorage, LikeStorage likeStorage) {
public FilmService(@Qualifier("DbFilmStorage") FilmStorage filmStorage,
@Qualifier("DbUserStorage") UserStorage userStorage,
@Qualifier("DbLikeStorage") LikeStorage likeStorage,
@Qualifier("DbGenreStorage") GenreStorage genreStorage) {
this.filmStorage = filmStorage;
this.userStorage = userStorage;
this.likeStorage = likeStorage;
this.genreStorage = genreStorage;
}

/**
Expand Down Expand Up @@ -68,9 +76,13 @@ public Collection<Film> getAllFilms() {
*/
@Override
public Film addFilm(Film film) {
Film returnFilm = filmStorage.add(film);
likeStorage.initializeLikesSet(returnFilm.getId());
return returnFilm;
film = filmStorage.add(film);

if (film.getGenres() != null && !film.getGenres().isEmpty()) {
genreStorage.addGenresToFilm(film);
}

return film;
}

/**
Expand All @@ -87,7 +99,13 @@ public Film updateFilm(Film film) {
}
throwIfNotFound(film.getId());

return filmStorage.update(film);
film = filmStorage.update(film);

if (film.getGenres() != null && !film.getGenres().isEmpty()) {
genreStorage.updateFilmGenres(film);
}

return film;
}

/**
Expand All @@ -100,7 +118,7 @@ public Film updateFilm(Film film) {
public void deleteFilm(Long id) {
throwIfNotFound(id);
filmStorage.remove(id);
likeStorage.clearLikesSet(id);
likeStorage.clearLikes(id);
}

/**
Expand All @@ -119,9 +137,10 @@ public Film addLike(Long filmId, Long userId) {
}

Long newLikes = likeStorage.addLike(filmId, userId);
filmStorage.get(filmId).setLikes(newLikes);
Film film = filmStorage.get(filmId);
film.setLikes(newLikes);

return filmStorage.get(filmId);
return film;
}

/**
Expand All @@ -140,9 +159,10 @@ public Film removeLike(Long filmId, Long userId) {
}

Long newLikes = likeStorage.deleteLike(filmId, userId);
filmStorage.get(filmId).setLikes(newLikes);
Film film = filmStorage.get(filmId);
film.setLikes(newLikes);

return filmStorage.get(filmId);
return film;
}

/**
Expand All @@ -153,11 +173,7 @@ public Film removeLike(Long filmId, Long userId) {
*/
@Override
public Collection<Film> getTopFilms(Long count) {

return filmStorage.getAll().stream()
.sorted(Comparator.comparingLong(Film::getLikes).reversed())
.limit(count)
.toList();
return filmStorage.getTopFilms(count);
}

@Override
Expand Down
Loading