diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..183ee28
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,48 @@
+FROM maven:3.9.11-amazoncorretto-21-alpine AS builder
+
+# Set working directory
+WORKDIR /app
+
+COPY pom.xml ./
+COPY lombok.config ./
+
+# Set up Maven local repository for caching
+ENV MAVEN_OPTS="-Dmaven.repo.local=/app/.m2/repository"
+
+# Download dependencies (caching layer)
+RUN mvn dependency:go-offline -B
+
+# Copy source code
+COPY src ./src
+
+# Build the application (skip tests for faster build)
+RUN mvn clean package -DskipTests -Dcheckstyle.skip=true
+
+# Stage 2: Layers stage
+FROM amazoncorretto:21.0.8-alpine AS layers
+WORKDIR /app
+COPY --from=builder /app/target/*.jar app.jar
+# Extract layers from the JAR for better caching in runtime
+RUN java -Djarmode=layertools -jar app.jar extract
+
+# Stage 3: Runtime stage
+FROM amazoncorretto:21.0.8-alpine
+
+# Set non-root user for security
+RUN addgroup -g 1001 appgroup && adduser -u 1001 -G appgroup -s /bin/sh -D appuser
+USER appuser
+
+# Set working directory
+WORKDIR /app
+
+# Copy extracted layers from builder stage
+COPY --from=layers /app/dependencies/ ./
+COPY --from=layers /app/spring-boot-loader/ ./
+COPY --from=layers /app/snapshot-dependencies/ ./
+COPY --from=layers /app/application/ ./
+
+# Expose the application port
+EXPOSE 8080
+
+# Entry point to run the application
+ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]
diff --git a/README.md b/README.md
index 36857da..50a7f64 100644
--- a/README.md
+++ b/README.md
@@ -2,10 +2,46 @@
Repository for filmorate - social network for film rating. Written in java.
+# Contributors
+Search - [Alexander-skipper](https://github.com/Alexander-skipper)
+Feed - [n20va](https://github.com/n20va)
+Recommendations - [Basementdoor](https://github.com/basementdoor)
+Reviews, top popular films by year and genre - [Nikolay Aleksandrov](https://github.com/Alextrusk27)
+
# DB schema

+# Tables description
+This section provides a detailed description of each table in the database schema.
+
+* Events
+This table logs user activities such as likes, reviews, and friendships, serving as an audit or history trail for operations.
+* Friendships
+This table stores user-to-user friendship relationships.
+* Users
+This table holds user profile information for personalization.
+* Likes
+This table records users' likes on films, for recommendation systems or popularity tracking.
+* Film Reviews
+This table links users and films to their reviews, acting as a junction for many-to-many relationships.
+* Reviews
+This table stores the content and metadata of user reviews.
+* Review Likes
+This table tracks likes on reviews, similar to upvoting for helpfulness.
+* Films
+This core table contains movie details for the platform's catalog.
+* Film Directors
+This junction table associates films with their directors.
+* Directors
+This table lists film directors.
+* MPA Ratings
+This table defines Motion Picture Association ratings (e.g., G, PG, R).
+* Film Genres
+This junction table links films to genres.
+* Genres
+This table categorizes film genres (e.g., Action, Drama).
+
## Examples of database queries
### Get all films
@@ -86,5 +122,5 @@ WHERE u.user_id IN (SELECT user_id2
SELECT user_id2
FROM friendships
- WHERE user_id1 = 2
+ WHERE user_id1 = 2)
```
diff --git a/assets/readme/db_schema.svg b/assets/readme/db_schema.svg
index b08713a..cce2b77 100644
--- a/assets/readme/db_schema.svg
+++ b/assets/readme/db_schema.svg
@@ -1,4 +1,4 @@
-
\ No newline at end of file
+ }1*1*1*1*1*1*1*1*1**1*1*1111**1usersuser_idBIGINTemailVARCHAR(80)loginVARCHAR(20)nameVARCHAR(20)birthdayDATEeventstimestampTIMESTAMPuser_idBIGINTevent_typeENUM(LIKE,REVIEW,FRIEND)operationENUM(REMOVE,ADD,UPDATE)event_idBIGINTentity_idBIGINTgenresgenre_idBIGINTnameVARCHAR(64)mpa_ratingsmpa_idBIGINTnameVARCHAR(20)directorsdirector_idBIGINTnameVARCHAR(120)filmsfilm_idBIGINTnameVARCHAR(120)descriptionVARCHAR(200)release_dateDATEduration_in_minutesINTEGERmpa_idBIGINTfilm_directorsfilm_idBIGINTdirector_idBIGINTreviewsreview_idBIGINTcontentVARCHAR(500)is_positiveBOOLEANuseful_ratingBIGINTfilm_reviewsuser_idBIGINTfilm_idBIGINTreview_idBIGINTreview_likesreview_idBIGINTuser_idBIGINTis_positiveBOOLEANfilm_genresfilm_idBIGINTgenre_idBIGINTlikesuser_idBIGINTfilm_idBIGINTfriendshipsuser_id1BIGINTuser_id2BIGINT
\ No newline at end of file
diff --git a/docker-compose.yaml b/docker-compose.yaml
new file mode 100644
index 0000000..086972f
--- /dev/null
+++ b/docker-compose.yaml
@@ -0,0 +1,13 @@
+services:
+ filmorate:
+ build:
+ context: .
+ dockerfile: Dockerfile
+ ports:
+ - "8080:8080"
+ volumes:
+ - maven-repo:/app/.m2/repository
+ - ./db:/app/db
+
+volumes:
+ maven-repo: {}
diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/DirectorController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/DirectorController.java
new file mode 100644
index 0000000..1cb9e1e
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/controller/DirectorController.java
@@ -0,0 +1,55 @@
+package ru.yandex.practicum.filmorate.controller;
+
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.Positive;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+import ru.yandex.practicum.filmorate.dto.director.DirectorDto;
+import ru.yandex.practicum.filmorate.dto.director.NewDirectorRequest;
+import ru.yandex.practicum.filmorate.dto.director.UpdateDirectorRequest;
+import ru.yandex.practicum.filmorate.service.director.DirectorService;
+
+import java.util.Collection;
+
+@RestController
+@RequestMapping("/directors")
+@Slf4j
+@RequiredArgsConstructor
+@Validated
+public class DirectorController {
+ private final DirectorService directorService;
+
+ @GetMapping
+ public Collection findAll() {
+ log.trace("Collection of all directors requested");
+ return directorService.findAll();
+ }
+
+ @GetMapping("/{id}")
+ public DirectorDto findById(@PathVariable @Positive long id) {
+ log.trace("Find director by id requested, id: {}", id);
+ return directorService.findById(id);
+ }
+
+ @PostMapping
+ @ResponseStatus(HttpStatus.CREATED)
+ public DirectorDto create(@RequestBody @Valid NewDirectorRequest request) {
+ log.trace("Create new director requested: {}", request);
+ return directorService.create(request);
+ }
+
+ @PutMapping
+ public DirectorDto update(@RequestBody @Valid UpdateDirectorRequest request) {
+ log.trace("Update director requested: {}", request);
+ return directorService.update(request);
+ }
+
+ @DeleteMapping("/{id}")
+ public void deleteById(@PathVariable @Positive long id) {
+ log.trace("Delete director by id requested, id: {}", id);
+ directorService.deleteById(id);
+ }
+}
diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/FeedController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/FeedController.java
new file mode 100644
index 0000000..b5fad4b
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/controller/FeedController.java
@@ -0,0 +1,23 @@
+package ru.yandex.practicum.filmorate.controller;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+import ru.yandex.practicum.filmorate.dto.event.EventDto;
+import ru.yandex.practicum.filmorate.service.feed.FeedService;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/users")
+@Slf4j
+@RequiredArgsConstructor
+public class FeedController {
+ private final FeedService feedService;
+
+ @GetMapping("/{id}/feed")
+ public List getUserFeed(@PathVariable long id) {
+ log.info("GET /users/{}/feed", id);
+ return feedService.getUserFeed(id);
+ }
+}
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 3852fc6..d82a7e3 100644
--- a/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java
+++ b/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java
@@ -1,6 +1,7 @@
package ru.yandex.practicum.filmorate.controller;
import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Positive;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -10,9 +11,10 @@
import ru.yandex.practicum.filmorate.dto.film.FilmDto;
import ru.yandex.practicum.filmorate.dto.film.NewFilmRequest;
import ru.yandex.practicum.filmorate.dto.film.UpdateFilmRequest;
-import ru.yandex.practicum.filmorate.service.FilmService;
+import ru.yandex.practicum.filmorate.service.film.FilmService;
import java.util.Collection;
+import java.util.List;
@RestController
@RequestMapping("/films")
@@ -42,36 +44,65 @@ public void deleteById(@PathVariable @Positive long filmId) {
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
- @Validated
public FilmDto create(@RequestBody @Valid NewFilmRequest request) {
log.trace("Create new film requested {}", request);
return filmService.create(request);
}
@PutMapping
- @Validated
public FilmDto update(@RequestBody @Valid UpdateFilmRequest request) {
log.trace("Update film requested {}", request);
return filmService.update(request);
}
@PutMapping("/{id}/like/{userId}")
- public void addLike(@PathVariable @Positive long id,
- @PathVariable @Positive long userId) {
+ public void addLike(@PathVariable long id,
+ @PathVariable long userId) {
log.trace("Add like requested film id: {}, user id: {}", id, userId);
filmService.addLike(id, userId);
}
@DeleteMapping("/{id}/like/{userId}")
- public void deleteLike(@PathVariable @Positive long id,
- @PathVariable @Positive long userId) {
+ public void deleteLike(@PathVariable long id,
+ @PathVariable long userId) {
log.trace("Delete like requested film id: {}, user id: {}", id, userId);
filmService.removeLike(id, userId);
}
+ @GetMapping("/search")
+ public Collection searchFilms(@RequestParam @NotBlank String query,
+ @RequestParam(defaultValue = "title") String by) {
+ log.trace("Search films requested with query: {}, by: {}", query, by);
+
+ List parsedInput = SearchFilmsBy.parseStrOrThrow(by, () ->
+ new IllegalArgumentException("Invalid search films requested: " + by));
+
+ return filmService.searchFilms(query, parsedInput);
+ }
+
+ @GetMapping("/common")
+ public Collection findCommonFilms(@RequestParam(name = "userId") @Positive long userId,
+ @RequestParam(name = "friendId") @Positive long friendId) {
+ log.trace("Find common films requested for user {} and friend {}", userId, friendId);
+ return filmService.findCommonFilms(userId, friendId);
+ }
+
@GetMapping("/popular")
- public Collection findPopular(@RequestParam(defaultValue = "10") @Positive int count) {
- log.trace("Find popular film requested with count: {}", count);
- return filmService.findFilmsWithTopLikes(count);
+ public Collection findPopular(@RequestParam(defaultValue = "10") @Positive int count,
+ @RequestParam(required = false) @Positive Integer genreId,
+ @RequestParam(required = false) @Positive Integer year) {
+ log.trace("Find popular film requested with count: {} by genre: {} for year: {}", count, genreId, year);
+ return filmService.findFilmsWithTopLikes(count, genreId, year);
+ }
+
+ @GetMapping("/director/{directorId}")
+ public Collection findFilmsOfDirector(@PathVariable @Positive long directorId,
+ @RequestParam String sortBy) {
+
+ FilmsSortBy sortFilmsBy = FilmsSortBy.fromString(sortBy).orElseThrow(
+ () -> new IllegalArgumentException("invalid sort by: %s".formatted(sortBy)));
+
+ log.trace("Find films of director with id {} requested", directorId);
+ return filmService.findFilmsOfDirector(directorId, sortFilmsBy);
}
}
diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/FilmsSortBy.java b/src/main/java/ru/yandex/practicum/filmorate/controller/FilmsSortBy.java
new file mode 100644
index 0000000..dc2fda2
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/controller/FilmsSortBy.java
@@ -0,0 +1,16 @@
+package ru.yandex.practicum.filmorate.controller;
+
+import java.util.Optional;
+
+public enum FilmsSortBy {
+ YEAR,
+ LIKES;
+
+ public static Optional fromString(String sortBy) {
+ return switch (sortBy.toLowerCase()) {
+ case "year" -> Optional.of(YEAR);
+ case "likes" -> Optional.of(LIKES);
+ default -> Optional.empty();
+ };
+ }
+}
diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/GenreController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/GenreController.java
index 0151f21..15f0ad9 100644
--- a/src/main/java/ru/yandex/practicum/filmorate/controller/GenreController.java
+++ b/src/main/java/ru/yandex/practicum/filmorate/controller/GenreController.java
@@ -9,7 +9,7 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import ru.yandex.practicum.filmorate.dto.genre.GenreDto;
-import ru.yandex.practicum.filmorate.service.GenreService;
+import ru.yandex.practicum.filmorate.service.genre.GenreService;
import java.util.Collection;
diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/MPARatingController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/MPARatingController.java
index 3eee3ce..236b665 100644
--- a/src/main/java/ru/yandex/practicum/filmorate/controller/MPARatingController.java
+++ b/src/main/java/ru/yandex/practicum/filmorate/controller/MPARatingController.java
@@ -9,7 +9,7 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import ru.yandex.practicum.filmorate.dto.mparating.MPARatingDto;
-import ru.yandex.practicum.filmorate.service.MPARatingService;
+import ru.yandex.practicum.filmorate.service.mparating.MPARatingService;
import java.util.Collection;
diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/ReviewController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/ReviewController.java
new file mode 100644
index 0000000..a081a78
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/controller/ReviewController.java
@@ -0,0 +1,85 @@
+package ru.yandex.practicum.filmorate.controller;
+
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.Positive;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+import ru.yandex.practicum.filmorate.dto.review.NewReviewRequest;
+import ru.yandex.practicum.filmorate.dto.review.ReviewDto;
+import ru.yandex.practicum.filmorate.dto.review.UpdateReviewRequest;
+import ru.yandex.practicum.filmorate.model.Reaction;
+import ru.yandex.practicum.filmorate.service.review.ReviewService;
+
+import java.util.Collection;
+
+@RestController
+@RequestMapping("/reviews")
+@Slf4j
+@Validated
+@RequiredArgsConstructor
+public class ReviewController {
+ private final ReviewService reviewService;
+
+ @PostMapping
+ @ResponseStatus(HttpStatus.CREATED)
+ public ReviewDto create(@Valid @RequestBody NewReviewRequest request) {
+ log.trace("Create review request: {}", request);
+ return reviewService.create(request);
+ }
+
+ @PutMapping
+ public ReviewDto update(@Valid @RequestBody UpdateReviewRequest request) {
+ log.trace("Update review request: {}", request);
+ return reviewService.update(request);
+ }
+
+ @DeleteMapping("/{id}")
+ public void delete(@PathVariable @Positive long id) {
+ log.trace("Delete review request by reviewId: {}", id);
+ reviewService.delete(id);
+ }
+
+ @GetMapping("/{id}")
+ public ReviewDto findById(@PathVariable @Positive long id) {
+ log.trace("Find review by reviewId: {}", id);
+ return reviewService.findById(id);
+ }
+
+ @GetMapping
+ public Collection findAllByFilm(@RequestParam(required = false) @Positive Long filmId,
+ @RequestParam(defaultValue = "10") @Positive long count) {
+ log.trace("Find all reviews by film Id: {}, count: {}", filmId, count);
+ return reviewService.findAllByFilm(filmId, count);
+ }
+
+ @PutMapping("/{id}/like/{userId}")
+ public void setLike(@PathVariable @Positive long id,
+ @PathVariable @Positive long userId) {
+ log.trace("Set review like by reviewId: {}, from userId: {}", id, userId);
+ reviewService.setLike(id, userId);
+ }
+
+ @PutMapping("/{id}/dislike/{userId}")
+ public void setDislike(@PathVariable @Positive long id,
+ @PathVariable @Positive long userId) {
+ log.trace("Set review dislike by reviewId: {}, from userId: {}", id, userId);
+ reviewService.setDislike(id, userId);
+ }
+
+ @DeleteMapping("/{id}/like/{userId}")
+ public void removeLike(@PathVariable @Positive long id,
+ @PathVariable @Positive long userId) {
+ log.trace("Remove review like by reviewId: {}, from userId: {}", id, userId);
+ reviewService.removeReaction(id, userId, Reaction.LIKE);
+ }
+
+ @DeleteMapping("/{id}/dislike/{userId}")
+ public void removeDislike(@PathVariable @Positive long id,
+ @PathVariable @Positive long userId) {
+ log.trace("Remove review dislike by reviewId: {}, from userId: {}", id, userId);
+ reviewService.removeReaction(id, userId, Reaction.DISLIKE);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/SearchFilmsBy.java b/src/main/java/ru/yandex/practicum/filmorate/controller/SearchFilmsBy.java
new file mode 100644
index 0000000..99c1434
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/controller/SearchFilmsBy.java
@@ -0,0 +1,43 @@
+package ru.yandex.practicum.filmorate.controller;
+
+import lombok.Getter;
+
+import java.awt.*;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Supplier;
+
+@Getter
+public enum SearchFilmsBy {
+ TITLE("title"),
+ DIRECTOR("director");
+
+ private final String value;
+
+ SearchFilmsBy(String value) {
+ this.value = value;
+ }
+
+ public static List parseStrOrThrow(String value,
+ Supplier exceptionSupplier) throws T {
+ String[] splittedStr = value.trim().toLowerCase().split(",");
+ List parsedList = new ArrayList<>();
+
+ for (String str : splittedStr) {
+ parsedList.add(SearchFilmsBy.fromString(str)
+ .orElseThrow(exceptionSupplier));
+ }
+
+ return parsedList;
+ }
+
+ public static Optional fromString(String value) {
+ return switch (value) {
+ case "title" -> Optional.of(TITLE);
+ case "director" -> Optional.of(DIRECTOR);
+ default -> Optional.empty();
+ };
+ }
+
+}
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 4e8063f..a4ca3c7 100644
--- a/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java
+++ b/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java
@@ -7,10 +7,12 @@
import org.springframework.http.HttpStatus;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
+import ru.yandex.practicum.filmorate.dto.film.FilmDto;
import ru.yandex.practicum.filmorate.dto.user.NewUserRequest;
import ru.yandex.practicum.filmorate.dto.user.UpdateUserRequest;
import ru.yandex.practicum.filmorate.dto.user.UserDto;
-import ru.yandex.practicum.filmorate.service.UserService;
+import ru.yandex.practicum.filmorate.service.recommendation.RecommendationService;
+import ru.yandex.practicum.filmorate.service.user.UserService;
import java.util.Collection;
@@ -22,6 +24,7 @@
@Validated
public class UserController {
private final UserService userService;
+ private final RecommendationService recommendationService;
@GetMapping
public Collection findAll() {
@@ -43,22 +46,20 @@ public void deleteById(@PathVariable @Positive long userId) {
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
- @Validated
public UserDto create(@RequestBody @Valid NewUserRequest user) {
log.trace("create new user requested {}", user);
return userService.create(user);
}
@PutMapping
- @Validated
public UserDto update(@RequestBody @Valid UpdateUserRequest user) {
log.trace("Update user requested {}", user);
return userService.update(user);
}
@PutMapping("/{id}/friends/{friendId}")
- public void addFriend(@PathVariable @Positive long id,
- @PathVariable @Positive long friendId) {
+ public void addFriend(@PathVariable long id,
+ @PathVariable long friendId) {
log.trace("Add friend requested for id {}, friend id: {}", id, friendId);
userService.addFriend(id, friendId);
}
@@ -82,4 +83,10 @@ public Collection findCommonFriends(@PathVariable @Positive long id,
log.trace("Find common friends requested for id {}, other id: {}", id, otherId);
return userService.findAllCommonFriends(id, otherId);
}
+
+ @GetMapping("/{id}/recommendations")
+ public Collection findFilmRecommendations(@PathVariable @Positive long id) {
+ log.trace("Find film recommendations requested for id {}", id);
+ return recommendationService.findFilmRecommendations(id);
+ }
}
diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/handler/ExceptionApiHandler.java b/src/main/java/ru/yandex/practicum/filmorate/controller/handler/ExceptionApiHandler.java
index 63381ab..553c3d6 100644
--- a/src/main/java/ru/yandex/practicum/filmorate/controller/handler/ExceptionApiHandler.java
+++ b/src/main/java/ru/yandex/practicum/filmorate/controller/handler/ExceptionApiHandler.java
@@ -3,6 +3,7 @@
import jakarta.validation.ConstraintViolationException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
+import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
@@ -27,6 +28,20 @@ public ErrorResponse handleException(Exception ex) {
"An error occurred while processing request");
}
+ @ExceptionHandler(IllegalArgumentException.class)
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ public ErrorResponse handleIllegalArgumentException(IllegalArgumentException e) {
+ log.warn(e.getMessage());
+ return new ErrorResponse("Bad request", e.getMessage());
+ }
+
+ @ExceptionHandler(HttpMessageNotReadableException.class)
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ public ErrorResponse handleHttpMessageNotReadableException(HttpMessageNotReadableException e) {
+ log.warn(e.getMessage());
+ return new ErrorResponse("bad request", "Request body is not readable");
+ }
+
@ExceptionHandler(ConstraintViolationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ValidationErrorResponse onConstraintValidationException(
diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/director/DirectorDto.java b/src/main/java/ru/yandex/practicum/filmorate/dto/director/DirectorDto.java
new file mode 100644
index 0000000..d62ab7c
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/dto/director/DirectorDto.java
@@ -0,0 +1,7 @@
+package ru.yandex.practicum.filmorate.dto.director;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record DirectorDto(Long id, String name) {
+}
diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/director/NewDirectorRequest.java b/src/main/java/ru/yandex/practicum/filmorate/dto/director/NewDirectorRequest.java
new file mode 100644
index 0000000..8f118b8
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/dto/director/NewDirectorRequest.java
@@ -0,0 +1,13 @@
+package ru.yandex.practicum.filmorate.dto.director;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.AccessLevel;
+import lombok.Data;
+import lombok.experimental.FieldDefaults;
+
+@Data
+@FieldDefaults(level = AccessLevel.PRIVATE)
+public class NewDirectorRequest {
+ @NotBlank
+ String name;
+}
diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/director/UpdateDirectorRequest.java b/src/main/java/ru/yandex/practicum/filmorate/dto/director/UpdateDirectorRequest.java
new file mode 100644
index 0000000..f3602cb
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/dto/director/UpdateDirectorRequest.java
@@ -0,0 +1,18 @@
+package ru.yandex.practicum.filmorate.dto.director;
+
+import jakarta.validation.constraints.NotNull;
+import lombok.AccessLevel;
+import lombok.Data;
+import lombok.experimental.FieldDefaults;
+
+@Data
+@FieldDefaults(level = AccessLevel.PRIVATE)
+public class UpdateDirectorRequest {
+ @NotNull
+ Long id;
+ String name;
+
+ public boolean hasName() {
+ return name != null && !name.isEmpty();
+ }
+}
diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/error/ErrorResponse.java b/src/main/java/ru/yandex/practicum/filmorate/dto/error/ErrorResponse.java
index 9d2d3b4..35c4cfa 100644
--- a/src/main/java/ru/yandex/practicum/filmorate/dto/error/ErrorResponse.java
+++ b/src/main/java/ru/yandex/practicum/filmorate/dto/error/ErrorResponse.java
@@ -1,4 +1,4 @@
package ru.yandex.practicum.filmorate.dto.error;
-public record ErrorResponse(String errorName, String description) {
+public record ErrorResponse(String error, String description) {
}
diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/event/EventDto.java b/src/main/java/ru/yandex/practicum/filmorate/dto/event/EventDto.java
new file mode 100644
index 0000000..306c8f6
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/dto/event/EventDto.java
@@ -0,0 +1,13 @@
+package ru.yandex.practicum.filmorate.dto.event;
+
+import ru.yandex.practicum.filmorate.model.EventType;
+import ru.yandex.practicum.filmorate.model.Operation;
+
+public record EventDto(Long timestamp,
+ Long userId,
+ EventType eventType,
+ Operation operation,
+ Long eventId,
+ Long entityId) {
+
+}
diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/film/FilmDto.java b/src/main/java/ru/yandex/practicum/filmorate/dto/film/FilmDto.java
index f9ffcc8..58753ed 100644
--- a/src/main/java/ru/yandex/practicum/filmorate/dto/film/FilmDto.java
+++ b/src/main/java/ru/yandex/practicum/filmorate/dto/film/FilmDto.java
@@ -1,26 +1,19 @@
package ru.yandex.practicum.filmorate.dto.film;
-import lombok.AccessLevel;
-import lombok.Builder;
-import lombok.Data;
-import lombok.Singular;
-import lombok.experimental.FieldDefaults;
+import ru.yandex.practicum.filmorate.dto.director.DirectorDto;
import ru.yandex.practicum.filmorate.dto.genre.GenreDto;
import ru.yandex.practicum.filmorate.dto.mparating.MPARatingDto;
import java.time.LocalDate;
import java.util.List;
-@Data
-@Builder
-@FieldDefaults(level = AccessLevel.PRIVATE)
-public class FilmDto {
- Long id;
- String name;
- String description;
- LocalDate releaseDate;
- Integer duration;
- MPARatingDto mpa;
- @Singular
- List genres;
+public record FilmDto(
+ Long id,
+ String name,
+ String description,
+ LocalDate releaseDate,
+ Integer duration,
+ MPARatingDto mpa,
+ List genres,
+ List directors) {
}
diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/film/NewFilmRequest.java b/src/main/java/ru/yandex/practicum/filmorate/dto/film/NewFilmRequest.java
index fa9f827..b256069 100644
--- a/src/main/java/ru/yandex/practicum/filmorate/dto/film/NewFilmRequest.java
+++ b/src/main/java/ru/yandex/practicum/filmorate/dto/film/NewFilmRequest.java
@@ -9,6 +9,7 @@
import lombok.AccessLevel;
import lombok.Data;
import lombok.experimental.FieldDefaults;
+import ru.yandex.practicum.filmorate.dto.director.DirectorDto;
import ru.yandex.practicum.filmorate.dto.genre.GenreDto;
import ru.yandex.practicum.filmorate.dto.mparating.MPARatingDto;
import ru.yandex.practicum.filmorate.validation.AfterDate;
@@ -34,4 +35,6 @@ public class NewFilmRequest {
MPARatingDto mpa;
@JsonSetter(nulls = Nulls.AS_EMPTY)
List genres = new ArrayList<>();
+ @JsonSetter(nulls = Nulls.AS_EMPTY)
+ List directors = new ArrayList<>();
}
diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/film/UpdateFilmRequest.java b/src/main/java/ru/yandex/practicum/filmorate/dto/film/UpdateFilmRequest.java
index 90a5902..2f7c0d7 100644
--- a/src/main/java/ru/yandex/practicum/filmorate/dto/film/UpdateFilmRequest.java
+++ b/src/main/java/ru/yandex/practicum/filmorate/dto/film/UpdateFilmRequest.java
@@ -1,16 +1,20 @@
package ru.yandex.practicum.filmorate.dto.film;
+import com.fasterxml.jackson.annotation.JsonSetter;
+import com.fasterxml.jackson.annotation.Nulls;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.Size;
import lombok.AccessLevel;
import lombok.Data;
import lombok.experimental.FieldDefaults;
+import ru.yandex.practicum.filmorate.dto.director.DirectorDto;
import ru.yandex.practicum.filmorate.dto.genre.GenreDto;
import ru.yandex.practicum.filmorate.dto.mparating.MPARatingDto;
import ru.yandex.practicum.filmorate.validation.AfterDate;
import java.time.LocalDate;
+import java.util.ArrayList;
import java.util.List;
@Data
@@ -27,6 +31,8 @@ public class UpdateFilmRequest {
Integer duration;
MPARatingDto mpa;
List genres;
+ @JsonSetter(nulls = Nulls.AS_EMPTY)
+ List directors = new ArrayList<>();
public boolean hasName() {
return name != null && !name.isEmpty();
@@ -45,10 +51,14 @@ public boolean hasDuration() {
}
public boolean hasGenres() {
- return genres != null && !genres.isEmpty();
+ return genres != null;
}
public boolean hasMpa() {
return mpa != null && mpa.id() != null;
}
+
+ public boolean hasDirectors() {
+ return directors != null;
+ }
}
diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/review/NewReviewRequest.java b/src/main/java/ru/yandex/practicum/filmorate/dto/review/NewReviewRequest.java
new file mode 100644
index 0000000..5115277
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/dto/review/NewReviewRequest.java
@@ -0,0 +1,25 @@
+package ru.yandex.practicum.filmorate.dto.review;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import lombok.AccessLevel;
+import lombok.Data;
+import lombok.experimental.FieldDefaults;
+
+@Data
+@FieldDefaults(level = AccessLevel.PRIVATE)
+public class NewReviewRequest {
+ @NotBlank(message = "Text must not be null or empty")
+ @Size(message = "Text length must be less than 500", max = 500)
+ String content;
+
+ @NotNull(message = "Rating must not be null")
+ Boolean isPositive;
+
+ @NotNull(message = "User ID must be present")
+ Long userId;
+
+ @NotNull(message = "Film ID must be present")
+ Long filmId;
+}
\ No newline at end of file
diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/review/ReviewDto.java b/src/main/java/ru/yandex/practicum/filmorate/dto/review/ReviewDto.java
new file mode 100644
index 0000000..62c89a2
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/dto/review/ReviewDto.java
@@ -0,0 +1,10 @@
+package ru.yandex.practicum.filmorate.dto.review;
+
+public record ReviewDto(
+ Long reviewId,
+ String content,
+ Boolean isPositive,
+ Long userId,
+ Long filmId,
+ Long useful) {
+}
diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/review/UpdateReviewRequest.java b/src/main/java/ru/yandex/practicum/filmorate/dto/review/UpdateReviewRequest.java
new file mode 100644
index 0000000..e4e0ada
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/dto/review/UpdateReviewRequest.java
@@ -0,0 +1,29 @@
+package ru.yandex.practicum.filmorate.dto.review;
+
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import lombok.AccessLevel;
+import lombok.Builder;
+import lombok.Data;
+import lombok.experimental.FieldDefaults;
+
+@Data
+@Builder
+@FieldDefaults(level = AccessLevel.PRIVATE)
+public class UpdateReviewRequest {
+ @NotNull
+ Long reviewId;
+
+ @Size(message = "Text length must be less than 500", max = 500)
+ String content;
+
+ Boolean isPositive;
+
+ public boolean hasContent() {
+ return this.content != null && !this.content.isEmpty();
+ }
+
+ public boolean hasIsPositive() {
+ return this.isPositive != null;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/user/UserDto.java b/src/main/java/ru/yandex/practicum/filmorate/dto/user/UserDto.java
index 531d172..edd64ff 100644
--- a/src/main/java/ru/yandex/practicum/filmorate/dto/user/UserDto.java
+++ b/src/main/java/ru/yandex/practicum/filmorate/dto/user/UserDto.java
@@ -1,21 +1,11 @@
package ru.yandex.practicum.filmorate.dto.user;
-import com.fasterxml.jackson.annotation.JsonProperty;
-import lombok.AccessLevel;
-import lombok.Builder;
-import lombok.Data;
-import lombok.experimental.FieldDefaults;
-
import java.time.LocalDate;
-@Data
-@Builder
-@FieldDefaults(level = AccessLevel.PRIVATE)
-public class UserDto {
- @JsonProperty(access = JsonProperty.Access.READ_ONLY)
- long id;
- String email;
- String login;
- String name;
- LocalDate birthday;
+public record UserDto(
+ long id,
+ String email,
+ String login,
+ String name,
+ LocalDate birthday) {
}
diff --git a/src/main/java/ru/yandex/practicum/filmorate/mapper/DirectorMapper.java b/src/main/java/ru/yandex/practicum/filmorate/mapper/DirectorMapper.java
new file mode 100644
index 0000000..df423f6
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/mapper/DirectorMapper.java
@@ -0,0 +1,36 @@
+package ru.yandex.practicum.filmorate.mapper;
+
+import lombok.experimental.UtilityClass;
+import ru.yandex.practicum.filmorate.dto.director.DirectorDto;
+import ru.yandex.practicum.filmorate.dto.director.NewDirectorRequest;
+import ru.yandex.practicum.filmorate.dto.director.UpdateDirectorRequest;
+import ru.yandex.practicum.filmorate.model.Director;
+
+@UtilityClass
+public class DirectorMapper {
+
+ public DirectorDto toDirectorDto(Director director) {
+ return new DirectorDto(director.getId(), director.getName());
+ }
+
+ public Director toDirector(NewDirectorRequest directorDto) {
+ return Director.builder()
+ .name(directorDto.getName())
+ .build();
+ }
+
+ public Director toDirector(DirectorDto directorDto) {
+ return Director.builder()
+ .name(directorDto.name())
+ .id(directorDto.id())
+ .build();
+ }
+
+ public Director updateDirectorFields(Director director, UpdateDirectorRequest request) {
+ if (request.hasName()) {
+ director.setName(request.getName());
+ }
+
+ return director;
+ }
+}
diff --git a/src/main/java/ru/yandex/practicum/filmorate/mapper/EventMapper.java b/src/main/java/ru/yandex/practicum/filmorate/mapper/EventMapper.java
new file mode 100644
index 0000000..d02e99e
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/mapper/EventMapper.java
@@ -0,0 +1,19 @@
+package ru.yandex.practicum.filmorate.mapper;
+
+import lombok.experimental.UtilityClass;
+import ru.yandex.practicum.filmorate.dto.event.EventDto;
+import ru.yandex.practicum.filmorate.model.Event;
+
+@UtilityClass
+public class EventMapper {
+
+ public EventDto toEventDto(Event event) {
+ return new EventDto(
+ event.getTimestamp(),
+ event.getUserId(),
+ event.getEventType(),
+ event.getOperation(),
+ event.getEventId(),
+ event.getEntityId());
+ }
+}
diff --git a/src/main/java/ru/yandex/practicum/filmorate/mapper/FilmMapper.java b/src/main/java/ru/yandex/practicum/filmorate/mapper/FilmMapper.java
index 3be6adc..3a048bb 100644
--- a/src/main/java/ru/yandex/practicum/filmorate/mapper/FilmMapper.java
+++ b/src/main/java/ru/yandex/practicum/filmorate/mapper/FilmMapper.java
@@ -4,27 +4,26 @@
import ru.yandex.practicum.filmorate.dto.film.FilmDto;
import ru.yandex.practicum.filmorate.dto.film.NewFilmRequest;
import ru.yandex.practicum.filmorate.dto.film.UpdateFilmRequest;
-import ru.yandex.practicum.filmorate.dto.genre.GenreDto;
import ru.yandex.practicum.filmorate.model.Film;
-import ru.yandex.practicum.filmorate.model.Genre;
-
-import java.util.stream.Collectors;
@UtilityClass
public class FilmMapper {
public FilmDto toFilmDto(Film film) {
- return FilmDto.builder()
- .id(film.getId())
- .description(film.getDescription())
- .name(film.getName())
- .releaseDate(film.getReleaseDate())
- .duration(film.getDuration())
- .mpa(MPARatingMapper.toMPARatingDto(film.getMpaRating()))
- .genres(film.getGenres()
+ return new FilmDto(
+ film.getId(),
+ film.getName(),
+ film.getDescription(),
+ film.getReleaseDate(),
+ film.getDuration(),
+ MPARatingMapper.toMPARatingDto(film.getMpaRating()),
+ film.getGenres()
.stream()
- .map(genre -> new GenreDto(genre.getId(), genre.getName()))
- .toList())
- .build();
+ .map(GenreMapper::toGenreDto)
+ .toList(),
+ film.getDirectors()
+ .stream()
+ .map(DirectorMapper::toDirectorDto)
+ .toList());
}
public Film toFilm(NewFilmRequest request) {
@@ -36,11 +35,14 @@ public Film toFilm(NewFilmRequest request) {
.mpaRating(MPARatingMapper.toMPARating(request.getMpa()))
.genres(request.getGenres()
.stream()
- .map(genreDto -> Genre.builder()
- .id(genreDto.id())
- .name(genreDto.name())
- .build())
- .collect(Collectors.toSet()))
+ .map(GenreMapper::toGenre)
+ .distinct()
+ .toList())
+ .directors(request.getDirectors()
+ .stream()
+ .map(DirectorMapper::toDirector)
+ .distinct()
+ .toList())
.build();
}
@@ -64,13 +66,21 @@ public Film updateFilmFields(Film film, UpdateFilmRequest request) {
if (request.hasGenres()) {
film.setGenres(request.getGenres().stream()
.map(GenreMapper::toGenre)
- .collect(Collectors.toSet()));
+ .distinct()
+ .toList());
}
if (request.hasMpa()) {
film.setMpaRating(MPARatingMapper.toMPARating(request.getMpa()));
}
+ if (request.hasDirectors()) {
+ film.setDirectors(request.getDirectors().stream()
+ .map(DirectorMapper::toDirector)
+ .distinct()
+ .toList());
+ }
+
return film;
}
}
diff --git a/src/main/java/ru/yandex/practicum/filmorate/mapper/ReviewMapper.java b/src/main/java/ru/yandex/practicum/filmorate/mapper/ReviewMapper.java
new file mode 100644
index 0000000..1911f92
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/mapper/ReviewMapper.java
@@ -0,0 +1,39 @@
+package ru.yandex.practicum.filmorate.mapper;
+
+import lombok.experimental.UtilityClass;
+import ru.yandex.practicum.filmorate.dto.review.NewReviewRequest;
+import ru.yandex.practicum.filmorate.dto.review.ReviewDto;
+import ru.yandex.practicum.filmorate.dto.review.UpdateReviewRequest;
+import ru.yandex.practicum.filmorate.model.Review;
+
+@UtilityClass
+public class ReviewMapper {
+ public ReviewDto toDto(Review review) {
+ return new ReviewDto(
+ review.getId(),
+ review.getContent(),
+ review.getIsPositive(),
+ review.getUserId(),
+ review.getFilmId(),
+ review.getUsefulRating());
+ }
+
+ public Review toReview(NewReviewRequest request) {
+ return Review.builder()
+ .content(request.getContent())
+ .isPositive(request.getIsPositive())
+ .userId(request.getUserId())
+ .filmId(request.getFilmId())
+ .build();
+ }
+
+ public Review updateReviewFields(Review review, UpdateReviewRequest request) {
+ if (request.hasContent()) {
+ review.setContent(request.getContent());
+ }
+ if (request.hasIsPositive()) {
+ review.setIsPositive(request.getIsPositive());
+ }
+ return review;
+ }
+}
diff --git a/src/main/java/ru/yandex/practicum/filmorate/mapper/UserMapper.java b/src/main/java/ru/yandex/practicum/filmorate/mapper/UserMapper.java
index 099ad58..cc1391a 100644
--- a/src/main/java/ru/yandex/practicum/filmorate/mapper/UserMapper.java
+++ b/src/main/java/ru/yandex/practicum/filmorate/mapper/UserMapper.java
@@ -10,13 +10,12 @@
@UtilityClass
public class UserMapper {
public UserDto toUserDto(User user) {
- return UserDto.builder()
- .id(user.getId())
- .email(user.getEmail())
- .login(user.getLogin())
- .name(user.getName())
- .birthday(user.getBirthday())
- .build();
+ return new UserDto(
+ user.getId(),
+ user.getEmail(),
+ user.getLogin(),
+ user.getName(),
+ user.getBirthday());
}
public User mapToUser(NewUserRequest request) {
diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/Director.java b/src/main/java/ru/yandex/practicum/filmorate/model/Director.java
new file mode 100644
index 0000000..d38e6bf
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/model/Director.java
@@ -0,0 +1,14 @@
+package ru.yandex.practicum.filmorate.model;
+
+import lombok.AccessLevel;
+import lombok.Builder;
+import lombok.Data;
+import lombok.experimental.FieldDefaults;
+
+@Data
+@Builder
+@FieldDefaults(level = AccessLevel.PRIVATE)
+public class Director {
+ Long id;
+ String name;
+}
diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/Event.java b/src/main/java/ru/yandex/practicum/filmorate/model/Event.java
new file mode 100644
index 0000000..29dad68
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/model/Event.java
@@ -0,0 +1,18 @@
+package ru.yandex.practicum.filmorate.model;
+
+import lombok.AccessLevel;
+import lombok.Builder;
+import lombok.Data;
+import lombok.experimental.FieldDefaults;
+
+@Data
+@Builder
+@FieldDefaults(level = AccessLevel.PRIVATE)
+public class Event {
+ Long eventId;
+ Long timestamp;
+ Long userId;
+ EventType eventType;
+ Operation operation;
+ Long entityId;
+}
diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/EventType.java b/src/main/java/ru/yandex/practicum/filmorate/model/EventType.java
new file mode 100644
index 0000000..508d573
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/model/EventType.java
@@ -0,0 +1,5 @@
+package ru.yandex.practicum.filmorate.model;
+
+public enum EventType {
+ LIKE, REVIEW, FRIEND
+}
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 684c52c..82e0a3c 100644
--- a/src/main/java/ru/yandex/practicum/filmorate/model/Film.java
+++ b/src/main/java/ru/yandex/practicum/filmorate/model/Film.java
@@ -6,8 +6,8 @@
import lombok.experimental.FieldDefaults;
import java.time.LocalDate;
-import java.util.HashSet;
-import java.util.Set;
+import java.util.ArrayList;
+import java.util.List;
@Data
@Builder
@@ -20,11 +20,7 @@ public class Film {
Integer duration;
MPARating mpaRating;
@Builder.Default
- Set genres = new HashSet<>();
+ List genres = new ArrayList<>();
@Builder.Default
- Set usersWhoLiked = new HashSet<>();
-
- public void addGenre(Genre genre) {
- this.genres.add(genre);
- }
+ List directors = new ArrayList<>();
}
diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/Operation.java b/src/main/java/ru/yandex/practicum/filmorate/model/Operation.java
new file mode 100644
index 0000000..74ef03b
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/model/Operation.java
@@ -0,0 +1,5 @@
+package ru.yandex.practicum.filmorate.model;
+
+public enum Operation {
+ REMOVE, ADD, UPDATE
+}
diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/Reaction.java b/src/main/java/ru/yandex/practicum/filmorate/model/Reaction.java
new file mode 100644
index 0000000..a7b7603
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/model/Reaction.java
@@ -0,0 +1,5 @@
+package ru.yandex.practicum.filmorate.model;
+
+public enum Reaction {
+ LIKE, DISLIKE
+}
\ No newline at end of file
diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/Review.java b/src/main/java/ru/yandex/practicum/filmorate/model/Review.java
new file mode 100644
index 0000000..983b06d
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/model/Review.java
@@ -0,0 +1,18 @@
+package ru.yandex.practicum.filmorate.model;
+
+import lombok.AccessLevel;
+import lombok.Builder;
+import lombok.Data;
+import lombok.experimental.FieldDefaults;
+
+@Data
+@Builder
+@FieldDefaults(level = AccessLevel.PRIVATE)
+public class Review {
+ Long id;
+ String content;
+ Boolean isPositive;
+ Long userId;
+ Long filmId;
+ Long usefulRating;
+}
diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/director/DirectorRepository.java b/src/main/java/ru/yandex/practicum/filmorate/repository/director/DirectorRepository.java
new file mode 100644
index 0000000..b26d76d
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/repository/director/DirectorRepository.java
@@ -0,0 +1,21 @@
+package ru.yandex.practicum.filmorate.repository.director;
+
+import ru.yandex.practicum.filmorate.model.Director;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+
+public interface DirectorRepository {
+ Collection findAll();
+
+ Optional findById(long id);
+
+ Director create(Director director);
+
+ void update(Director director);
+
+ void deleteById(long id);
+
+ List getByIds(List directorIds);
+}
diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/director/DirectorRowMapper.java b/src/main/java/ru/yandex/practicum/filmorate/repository/director/DirectorRowMapper.java
new file mode 100644
index 0000000..b7f45b5
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/repository/director/DirectorRowMapper.java
@@ -0,0 +1,19 @@
+package ru.yandex.practicum.filmorate.repository.director;
+
+import org.springframework.jdbc.core.RowMapper;
+import org.springframework.stereotype.Component;
+import ru.yandex.practicum.filmorate.model.Director;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+@Component
+public class DirectorRowMapper implements RowMapper {
+ @Override
+ public Director mapRow(ResultSet rs, int rowNum) throws SQLException {
+ return Director.builder()
+ .id(rs.getLong("director_id"))
+ .name(rs.getString("name"))
+ .build();
+ }
+}
diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/director/JdbcDirectorRepository.java b/src/main/java/ru/yandex/practicum/filmorate/repository/director/JdbcDirectorRepository.java
new file mode 100644
index 0000000..122404f
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/repository/director/JdbcDirectorRepository.java
@@ -0,0 +1,81 @@
+package ru.yandex.practicum.filmorate.repository.director;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.jdbc.core.RowMapper;
+import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
+import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
+import org.springframework.jdbc.support.GeneratedKeyHolder;
+import org.springframework.stereotype.Repository;
+import ru.yandex.practicum.filmorate.model.Director;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+
+@Repository
+@RequiredArgsConstructor
+public class JdbcDirectorRepository implements DirectorRepository {
+ private final NamedParameterJdbcTemplate jdbc;
+ private final RowMapper rowMapper;
+
+ @Override
+ public Collection findAll() {
+ String selectAllDirectorsSql = "SELECT * FROM directors ORDER BY director_id";
+ return jdbc.query(selectAllDirectorsSql, rowMapper);
+ }
+
+ @Override
+ public Optional findById(long id) {
+ String selectDirectorByIdSql = "SELECT * FROM directors WHERE director_id = :id";
+ MapSqlParameterSource params = new MapSqlParameterSource()
+ .addValue("id", id);
+
+ List directors = jdbc.query(selectDirectorByIdSql, params, rowMapper);
+
+ return directors.isEmpty() ? Optional.empty() : Optional.of(directors.getFirst());
+ }
+
+ @Override
+ public Director create(Director director) {
+ String saveDirectorSql = """
+ INSERT INTO directors (name)
+ VALUES (:name)""";
+ GeneratedKeyHolder keyHolder = new GeneratedKeyHolder();
+ MapSqlParameterSource params = new MapSqlParameterSource()
+ .addValue("name", director.getName());
+
+ jdbc.update(saveDirectorSql, params, keyHolder, new String[]{"director_id"});
+
+ director.setId(keyHolder.getKeyAs(Long.class));
+ return director;
+ }
+
+ @Override
+ public void update(Director director) {
+ String updateDirectorSql = """
+ UPDATE directors
+ SET name = :name
+ WHERE director_id = :id""";
+ MapSqlParameterSource params = new MapSqlParameterSource()
+ .addValue("name", director.getName())
+ .addValue("id", director.getId());
+ jdbc.update(updateDirectorSql, params);
+ }
+
+ @Override
+ public void deleteById(long id) {
+ String deleteDirectorByIdSql = "DELETE FROM directors WHERE director_id = :id";
+ MapSqlParameterSource params = new MapSqlParameterSource()
+ .addValue("id", id);
+
+ jdbc.update(deleteDirectorByIdSql, params);
+ }
+
+ @Override
+ public List getByIds(List directorIds) {
+ MapSqlParameterSource params = new MapSqlParameterSource()
+ .addValue("list", directorIds);
+ String selectAllDirectorsByIdsSql = "SELECT * FROM directors WHERE director_id IN (:list)";
+ return jdbc.query(selectAllDirectorsByIdsSql, params, rowMapper);
+ }
+}
diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/feed/EventRowMapper.java b/src/main/java/ru/yandex/practicum/filmorate/repository/feed/EventRowMapper.java
new file mode 100644
index 0000000..68994d6
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/repository/feed/EventRowMapper.java
@@ -0,0 +1,25 @@
+package ru.yandex.practicum.filmorate.repository.feed;
+
+import org.springframework.jdbc.core.RowMapper;
+import org.springframework.stereotype.Component;
+import ru.yandex.practicum.filmorate.model.Event;
+import ru.yandex.practicum.filmorate.model.EventType;
+import ru.yandex.practicum.filmorate.model.Operation;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+@Component
+public class EventRowMapper implements RowMapper {
+ @Override
+ public Event mapRow(ResultSet rs, int rowNum) throws SQLException {
+ return Event.builder()
+ .eventId(rs.getLong("event_id"))
+ .timestamp(rs.getLong("created_at"))
+ .userId(rs.getLong("user_id"))
+ .eventType(EventType.valueOf(rs.getString("event_type")))
+ .operation(Operation.valueOf(rs.getString("operation")))
+ .entityId(rs.getLong("entity_id"))
+ .build();
+ }
+}
diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/feed/FeedRepository.java b/src/main/java/ru/yandex/practicum/filmorate/repository/feed/FeedRepository.java
new file mode 100644
index 0000000..7645f79
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/repository/feed/FeedRepository.java
@@ -0,0 +1,10 @@
+package ru.yandex.practicum.filmorate.repository.feed;
+
+import ru.yandex.practicum.filmorate.model.Event;
+import java.util.List;
+
+public interface FeedRepository {
+ List findByUserId(long userId);
+
+ Event save(Event event);
+}
\ No newline at end of file
diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/feed/JdbcFeedRepository.java b/src/main/java/ru/yandex/practicum/filmorate/repository/feed/JdbcFeedRepository.java
new file mode 100644
index 0000000..cffbeda
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/repository/feed/JdbcFeedRepository.java
@@ -0,0 +1,43 @@
+package ru.yandex.practicum.filmorate.repository.feed;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
+import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
+import org.springframework.jdbc.support.GeneratedKeyHolder;
+import org.springframework.stereotype.Repository;
+import ru.yandex.practicum.filmorate.model.Event;
+
+import java.util.List;
+
+@Repository
+@RequiredArgsConstructor
+public class JdbcFeedRepository implements FeedRepository {
+ private final NamedParameterJdbcOperations jdbc;
+ private final EventRowMapper eventRowMapper;
+
+ @Override
+ public List findByUserId(long userId) {
+ String sql = "SELECT * FROM feed_events WHERE user_id = :user_id ORDER BY created_at ASC";
+ return jdbc.query(sql,
+ new MapSqlParameterSource("user_id", userId),
+ eventRowMapper);
+ }
+
+ @Override
+ public Event save(Event event) {
+ GeneratedKeyHolder keyHolder = new GeneratedKeyHolder();
+ MapSqlParameterSource params = new MapSqlParameterSource()
+ .addValue("timestamp", event.getTimestamp())
+ .addValue("user_id", event.getUserId())
+ .addValue("event_type", event.getEventType().toString())
+ .addValue("operation", event.getOperation().toString())
+ .addValue("entity_id", event.getEntityId());
+ String sql = """
+ INSERT INTO feed_events (created_at, user_id, event_type, operation, entity_id)
+ VALUES (:timestamp, :user_id, :event_type, :operation, :entity_id)
+ """;
+ jdbc.update(sql, params, keyHolder, new String[]{"event_id"});
+ event.setEventId(keyHolder.getKeyAs(Long.class));
+ return event;
+ }
+}
diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/film/FilmRepository.java b/src/main/java/ru/yandex/practicum/filmorate/repository/film/FilmRepository.java
index 3317862..32949af 100644
--- a/src/main/java/ru/yandex/practicum/filmorate/repository/film/FilmRepository.java
+++ b/src/main/java/ru/yandex/practicum/filmorate/repository/film/FilmRepository.java
@@ -1,8 +1,11 @@
package ru.yandex.practicum.filmorate.repository.film;
+import ru.yandex.practicum.filmorate.controller.FilmsSortBy;
+import ru.yandex.practicum.filmorate.controller.SearchFilmsBy;
import ru.yandex.practicum.filmorate.model.Film;
import java.util.Collection;
+import java.util.List;
import java.util.Optional;
public interface FilmRepository {
@@ -16,5 +19,13 @@ public interface FilmRepository {
void deleteById(long id);
- Collection findTopPopularFilms(int count);
+ Collection findTopPopularFilms(int count, Integer genreId, Integer year);
+
+ Collection findCommonFilms(long userId, long friendId);
+
+ Collection findFilmsOfDirector(long directorId, FilmsSortBy sortFilmsBy);
+
+ Collection findFilmRecommendations(long userId, List similarUserIds);
+
+ Collection searchFilms(String query, List searchBy);
}
diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/film/FilmResultSetExtractor.java b/src/main/java/ru/yandex/practicum/filmorate/repository/film/FilmResultSetExtractor.java
deleted file mode 100644
index 7b11ad9..0000000
--- a/src/main/java/ru/yandex/practicum/filmorate/repository/film/FilmResultSetExtractor.java
+++ /dev/null
@@ -1,66 +0,0 @@
-package ru.yandex.practicum.filmorate.repository.film;
-
-import org.springframework.jdbc.core.ResultSetExtractor;
-import org.springframework.stereotype.Component;
-import ru.yandex.practicum.filmorate.model.Film;
-import ru.yandex.practicum.filmorate.model.Genre;
-import ru.yandex.practicum.filmorate.model.MPARating;
-
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.time.LocalDate;
-import java.util.ArrayList;
-import java.util.List;
-
-@Component
-public class FilmResultSetExtractor implements ResultSetExtractor> {
-
- @Override
- public List extractData(ResultSet rs) throws SQLException {
- List films = new ArrayList<>();
- Film currentFilm = null;
- Long previousFilmId = null;
-
- while (rs.next()) {
- Long currentFilmId = rs.getLong("film_id");
-
- if (!currentFilmId.equals(previousFilmId)) {
- if (currentFilm != null) {
- films.add(currentFilm);
- }
-
- MPARating mpaRating = MPARating.builder()
- .id(rs.getLong("mpa_id"))
- .name(rs.getString("mpa_name"))
- .build();
-
- currentFilm = Film.builder()
- .id(currentFilmId)
- .name(rs.getString("name"))
- .description(rs.getString("description"))
- .releaseDate(rs.getObject("release_date", LocalDate.class))
- .duration(rs.getInt("duration_in_minutes"))
- .mpaRating(mpaRating)
- .build();
-
- previousFilmId = currentFilmId;
- }
-
-
-
- Genre genre = Genre.builder()
- .id(rs.getLong("genre_id"))
- .name(rs.getString("genre_name"))
- .build();
- if (!rs.wasNull()) {
- currentFilm.addGenre(genre);
- }
- }
-
- if (currentFilm != null) {
- films.add(currentFilm);
- }
-
- return films;
- }
-}
diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/film/FilmRowMapper.java b/src/main/java/ru/yandex/practicum/filmorate/repository/film/FilmRowMapper.java
index 5707b4a..c2bac65 100644
--- a/src/main/java/ru/yandex/practicum/filmorate/repository/film/FilmRowMapper.java
+++ b/src/main/java/ru/yandex/practicum/filmorate/repository/film/FilmRowMapper.java
@@ -2,12 +2,17 @@
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Component;
+import ru.yandex.practicum.filmorate.model.Director;
import ru.yandex.practicum.filmorate.model.Film;
+import ru.yandex.practicum.filmorate.model.Genre;
import ru.yandex.practicum.filmorate.model.MPARating;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDate;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
@Component
public class FilmRowMapper implements RowMapper {
@@ -17,6 +22,39 @@ public Film mapRow(ResultSet rs, int rowNum) throws SQLException {
.id(rs.getLong("mpa_id"))
.name(rs.getString("mpa_name"))
.build();
+
+ String genresString = rs.getString("genres");
+ List genres;
+ if (genresString != null && !genresString.isEmpty() && !genresString.startsWith(":")) {
+ genres = Arrays.stream(genresString.split(";"))
+ .map(genreIdNameStr -> {
+ String[] splitIdNameStr = genreIdNameStr.split(":");
+ return Genre.builder()
+ .id(Long.valueOf(splitIdNameStr[0]))
+ .name(splitIdNameStr[1])
+ .build();
+ })
+ .toList();
+ } else {
+ genres = Collections.emptyList();
+ }
+
+ String directorsString = rs.getString("directors");
+ List directors;
+ if (directorsString != null && !directorsString.isEmpty() && !directorsString.startsWith(":")) {
+ directors = Arrays.stream(directorsString.split(";"))
+ .map(directorIdNameStr -> {
+ String[] splitIdNameStr = directorIdNameStr.split(":");
+ return Director.builder()
+ .id(Long.valueOf(splitIdNameStr[0]))
+ .name(splitIdNameStr[1])
+ .build();
+ })
+ .toList();
+ } else {
+ directors = Collections.emptyList();
+ }
+
return Film.builder()
.id(rs.getLong("film_id"))
.name(rs.getString("name"))
@@ -24,6 +62,8 @@ public Film mapRow(ResultSet rs, int rowNum) throws SQLException {
.releaseDate(rs.getObject("release_date", LocalDate.class))
.duration(rs.getInt("duration_in_minutes"))
.mpaRating(mpaRating)
+ .genres(genres)
+ .directors(directors)
.build();
}
}
diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/film/JdbcFilmRepository.java b/src/main/java/ru/yandex/practicum/filmorate/repository/film/JdbcFilmRepository.java
index 73b73d2..0f3b4d7 100644
--- a/src/main/java/ru/yandex/practicum/filmorate/repository/film/JdbcFilmRepository.java
+++ b/src/main/java/ru/yandex/practicum/filmorate/repository/film/JdbcFilmRepository.java
@@ -1,94 +1,70 @@
package ru.yandex.practicum.filmorate.repository.film;
import lombok.RequiredArgsConstructor;
-import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.stereotype.Repository;
+import ru.yandex.practicum.filmorate.controller.FilmsSortBy;
+import ru.yandex.practicum.filmorate.controller.SearchFilmsBy;
+import ru.yandex.practicum.filmorate.model.Director;
import ru.yandex.practicum.filmorate.model.Film;
import ru.yandex.practicum.filmorate.model.Genre;
-import java.util.*;
-import java.util.function.Function;
-import java.util.stream.Collectors;
-
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
@Repository
@RequiredArgsConstructor
public class JdbcFilmRepository implements FilmRepository {
+ private static final String BASE_SELECT_SQL = """
+ SELECT f.film_id,
+ f.name,
+ f.description,
+ f.release_date,
+ f.duration_in_minutes,
+ f.mpa_id,
+ mr.name AS mpa_name,
+ (SELECT GROUP_CONCAT(DISTINCT CONCAT(g2.genre_id, ':', g2.name) ORDER BY g2.genre_id SEPARATOR ';')
+ FROM film_genres fg2
+ JOIN genres g2 ON g2.genre_id = fg2.genre_id
+ WHERE fg2.film_id = f.film_id) AS genres,
+ (SELECT GROUP_CONCAT(DISTINCT CONCAT(d2.director_id, ':', d2.name) ORDER BY d2.director_id SEPARATOR ';')
+ FROM film_directors fd2
+ JOIN directors d2 ON d2.director_id = fd2.director_id
+ WHERE fd2.film_id = f.film_id) AS directors
+ FROM films f
+ JOIN mpa_ratings mr ON f.mpa_id = mr.mpa_id
+ LEFT JOIN film_genres fg ON f.film_id = fg.film_id
+ LEFT JOIN genres g ON g.genre_id = fg.genre_id
+ LEFT JOIN film_directors fd on f.film_id = fd.film_id
+ LEFT JOIN directors d ON fd.director_id = d.director_id
+ """;
private final NamedParameterJdbcOperations jdbc;
- private final RowMapper genreRowMapper;
private final RowMapper filmRowMapper;
- private final ResultSetExtractor> filmResultSetExtractor;
@Override
public Optional findById(long id) {
MapSqlParameterSource params = new MapSqlParameterSource()
.addValue("id", id);
- String selectFilmByIdSql = """
- SELECT f.film_id,
- f.name,
- f.description,
- f.release_date,
- f.duration_in_minutes,
- f.mpa_id,
- mr.name AS mpa_name,
- g.genre_id,
- g.name AS genre_name
- FROM films f
- JOIN mpa_ratings mr ON f.mpa_id = mr.mpa_id
- LEFT JOIN film_genres fg ON f.film_id = fg.film_id
- LEFT JOIN genres g ON g.genre_id = fg.genre_id
- WHERE f.film_id = :id""";
+ String selectFilmByIdSql = BASE_SELECT_SQL.concat("""
+ WHERE f.film_id = :id
+ GROUP BY f.film_id""");
- List films = jdbc.query(selectFilmByIdSql, params, filmResultSetExtractor);
+ List films = jdbc.query(selectFilmByIdSql, params, filmRowMapper);
return films.isEmpty() ? Optional.empty() : Optional.of(films.getFirst());
}
@Override
public Collection findAll() {
- String filmsAndMpaSql = """
- SELECT f.film_id,
- f.name,
- f.description,
- f.release_date,
- f.duration_in_minutes,
- f.mpa_id,
- mr.name AS mpa_name
- FROM films f
- JOIN mpa_ratings mr ON f.mpa_id = mr.mpa_id""";
- Map films = jdbc.query(filmsAndMpaSql, filmRowMapper).stream()
- .collect(Collectors.toMap(
- Film::getId,
- Function.identity()
- ));
-
- String genresSql = "SELECT * FROM genres";
- Map genres = jdbc.query(genresSql, genreRowMapper).stream()
- .collect(Collectors.toMap(
- Genre::getId,
- Function.identity()
- ));
-
- String filmsGenresSql = """
- SELECT film_id,
- genre_id
- FROM film_genres""";
- List> filmsGenresList = jdbc.query(filmsGenresSql,
- (rs, rowNum) -> Map.entry(rs.getLong("film_id"),
- rs.getLong("genre_id"))
- );
-
- for (Map.Entry entry : filmsGenresList) {
- films.get(entry.getKey()).addGenre(genres.get(entry.getValue()));
- }
-
- return films.values();
+ String findAllSql = BASE_SELECT_SQL.concat("GROUP BY f.film_id");
+ return jdbc.query(findAllSql, filmRowMapper);
}
@Override
@@ -104,6 +80,7 @@ INSERT INTO films(name, description, release_date, duration_in_minutes, mpa_id)
film.setId(keyHolder.getKeyAs(Long.class));
saveGenres(film.getGenres(), film.getId());
+ saveDirectors(film.getDirectors(), film.getId());
return film;
}
@@ -120,11 +97,16 @@ public void update(Film film) {
String deleteFilmGenresSql = """
DELETE FROM film_genres WHERE film_id = :film_id""";
- MapSqlParameterSource deleteParams = new MapSqlParameterSource();
- deleteParams.addValue("film_id", film.getId());
- jdbc.update(deleteFilmGenresSql, deleteParams);
-
+ MapSqlParameterSource filmIdParams = new MapSqlParameterSource()
+ .addValue("film_id", film.getId());
+ jdbc.update(deleteFilmGenresSql, filmIdParams);
saveGenres(film.getGenres(), film.getId());
+
+ String deleteFilmDirectorsSql = """
+ DELETE FROM film_directors WHERE film_id = :film_id""";
+
+ jdbc.update(deleteFilmDirectorsSql, filmIdParams);
+ saveDirectors(film.getDirectors(), film.getId());
}
@Override
@@ -132,36 +114,117 @@ public void deleteById(long id) {
MapSqlParameterSource params = new MapSqlParameterSource()
.addValue("id", id);
String deleteFilmByIdSql = "DELETE FROM films WHERE film_id = :id";
+
jdbc.update(deleteFilmByIdSql, params);
}
@Override
- public Collection findTopPopularFilms(int count) {
- String selectTopFilmsSql = """
- SELECT f.film_id,
- f.name,
- f.description,
- f.release_date,
- f.duration_in_minutes,
- f.mpa_id,
- mr.name AS mpa_name,
- g.genre_id,
- g.name AS genre_name
- FROM films f
- JOIN mpa_ratings mr ON f.mpa_id = mr.mpa_id
- LEFT JOIN film_genres fg ON f.film_id = fg.film_id
- LEFT JOIN genres g ON g.genre_id = fg.genre_id
+ public Collection findTopPopularFilms(int count, Integer genreId, Integer year) {
+ String selectTopFilmsSql = BASE_SELECT_SQL.concat("""
+ LEFT JOIN likes fl ON f.film_id = fl.film_id
+ WHERE (g.genre_id = :genreId OR :genreId IS NULL)
+ AND (YEAR(f.release_date) =:year OR :year IS NULL)
+ GROUP BY f.film_id
+ ORDER BY COUNT(DISTINCT fl.user_id) DESC, f.film_id
+ LIMIT :count""");
+ MapSqlParameterSource params = new MapSqlParameterSource()
+ .addValue("count", count)
+ .addValue("genreId", genreId)
+ .addValue("year", year);
+
+ return jdbc.query(selectTopFilmsSql, params, filmRowMapper);
+ }
+
+ @Override
+ public Collection findCommonFilms(long userId, long friendId) {
+ String selectCommonFilmsSql = BASE_SELECT_SQL.concat("""
+ LEFT JOIN likes fl ON f.film_id = fl.film_id
+ WHERE f.film_id in (SELECT l1.film_id
+ FROM likes l1
+ WHERE l1.user_id = :user_id
+ INTERSECT
+ SELECT l2.film_id
+ FROM likes l2
+ WHERE l2.user_id = :friend_id)
+ GROUP BY f.film_id, g.genre_id
+ ORDER BY COUNT(DISTINCT fl.user_id) DESC, f.film_id
+ """);
+ MapSqlParameterSource params = new MapSqlParameterSource()
+ .addValue("user_id", userId)
+ .addValue("friend_id", friendId);
+
+ return jdbc.query(selectCommonFilmsSql, params, filmRowMapper);
+ }
+
+ @Override
+ public Collection findFilmsOfDirector(long directorId, FilmsSortBy sortFilmsBy) {
+ String sortBySql = switch (sortFilmsBy) {
+ case YEAR -> "EXTRACT(YEAR from f.release_date)";
+ case LIKES -> "COUNT(DISTINCT fl.user_id) DESC";
+ };
+
+ String selectFilmsOfDirectorSortedSql = BASE_SELECT_SQL.concat("""
LEFT JOIN likes fl ON f.film_id = fl.film_id
- GROUP BY f.film_id, f.name, g.genre_id
- ORDER BY COUNT(fl.user_id) DESC, f.film_id
- LIMIT :count""";
+ WHERE d.director_id = :director_id
+ GROUP BY f.film_id, g.genre_id, f.release_date
+ ORDER BY %s""".formatted(sortBySql)
+ );
MapSqlParameterSource params = new MapSqlParameterSource()
- .addValue("count", count);
+ .addValue("director_id", directorId);
+ return jdbc.query(selectFilmsOfDirectorSortedSql, params, filmRowMapper);
+ }
- return jdbc.query(selectTopFilmsSql, params, filmResultSetExtractor);
+ @Override
+ public Collection findFilmRecommendations(long userId, List similarUserIds) {
+ String sqlRecommendations = BASE_SELECT_SQL.concat("""
+ LEFT JOIN likes l ON f.film_id = l.film_id
+ WHERE l.user_id IN (:similarUserIds)
+ AND f.film_id NOT IN (
+ SELECT film_id FROM likes WHERE user_id = :userId
+ )
+ GROUP BY f.film_id
+ ORDER BY COUNT(DISTINCT l.user_id) DESC
+ """);
+
+ MapSqlParameterSource params = new MapSqlParameterSource()
+ .addValue("similarUserIds", similarUserIds)
+ .addValue("userId", userId);
+
+ return jdbc.query(sqlRecommendations, params, filmRowMapper);
}
- private void saveGenres(Set genres, long filmId) {
+ @Override
+ public Collection searchFilms(String query, List searchBy) {
+ String searchParamName = "query";
+ String searchFilmsSql = buildSearchSql(searchBy, searchParamName);
+ String searchPattern = "%%%s%%".formatted(query.toLowerCase());
+
+ MapSqlParameterSource params = new MapSqlParameterSource()
+ .addValue(searchParamName, searchPattern);
+
+ return jdbc.query(searchFilmsSql, params, filmRowMapper);
+ }
+
+ private String buildSearchSql(List searchFilmsBy, String searchParamName) {
+ List conditions = new ArrayList<>();
+
+ for (SearchFilmsBy searchBy : searchFilmsBy) {
+ switch (searchBy) {
+ case TITLE -> conditions.add("LOWER(f.name) LIKE :%s".formatted(searchParamName));
+ case DIRECTOR -> conditions.add("LOWER(d.name) LIKE :%s".formatted(searchParamName));
+ }
+ }
+
+ String whereClause = String.join(" OR ", conditions);
+
+ return BASE_SELECT_SQL.concat("""
+ WHERE (%s)
+ GROUP BY f.film_id
+ ORDER BY (SELECT COUNT(*) FROM likes l WHERE l.film_id = f.film_id) DESC, f.film_id""".formatted(
+ whereClause));
+ }
+
+ private void saveGenres(Collection genres, long filmId) {
String insertGenresSql = """
INSERT INTO film_genres (film_id, genre_id)
VALUES (:film_id, :genre_id)""";
@@ -176,6 +239,20 @@ INSERT INTO film_genres (film_id, genre_id)
jdbc.batchUpdate(insertGenresSql, batchParams);
}
+ private void saveDirectors(Collection directors, long filmId) {
+ String insertDirectorsSql = """
+ INSERT INTO film_directors (film_id, director_id)
+ VALUES (:film_id, :director_id)""";
+ SqlParameterSource[] batchParams = directors.stream()
+ .map(Director::getId)
+ .map(directorId -> new MapSqlParameterSource()
+ .addValue("film_id", filmId)
+ .addValue("director_id", directorId))
+ .toArray(SqlParameterSource[]::new);
+
+ jdbc.batchUpdate(insertDirectorsSql, batchParams);
+ }
+
private MapSqlParameterSource prepareParamMap(Film film) {
return new MapSqlParameterSource()
.addValue("name", film.getName())
diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/friendship/FriendshipsRepository.java b/src/main/java/ru/yandex/practicum/filmorate/repository/friendship/FriendshipsRepository.java
index 8f58c07..0dfeaa1 100644
--- a/src/main/java/ru/yandex/practicum/filmorate/repository/friendship/FriendshipsRepository.java
+++ b/src/main/java/ru/yandex/practicum/filmorate/repository/friendship/FriendshipsRepository.java
@@ -4,4 +4,6 @@ public interface FriendshipsRepository {
void addFriendship(long userId, long friendId);
void removeFriendship(long userId, long friendId);
+
+ boolean isFriends(long userId, long friendId);
}
diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/friendship/JdbcFriendshipsRepository.java b/src/main/java/ru/yandex/practicum/filmorate/repository/friendship/JdbcFriendshipsRepository.java
index 8fe0296..bf45d9c 100644
--- a/src/main/java/ru/yandex/practicum/filmorate/repository/friendship/JdbcFriendshipsRepository.java
+++ b/src/main/java/ru/yandex/practicum/filmorate/repository/friendship/JdbcFriendshipsRepository.java
@@ -28,9 +28,23 @@ public void removeFriendship(long userId, long friendId) {
jdbc.update(deleteFriendshipSql, params);
}
+ @Override
+ public boolean isFriends(long userId, long friendId) {
+ String checkFriendshipSql = """
+ SELECT EXISTS (
+ SELECT 1
+ FROM friendships
+ WHERE user_id1 = :user_id AND user_id2 = :friend_id)""";
+ MapSqlParameterSource params = new MapSqlParameterSource()
+ .addValue("user_id", userId)
+ .addValue("friend_id", friendId);
+
+ return Boolean.TRUE.equals(jdbc.queryForObject(checkFriendshipSql, params, Boolean.class));
+ }
+
private MapSqlParameterSource getParameterMap(long userId, long friendId) {
return new MapSqlParameterSource()
- .addValue("user_id1", userId)
- .addValue("user_id2", friendId);
+ .addValue("user_id1", userId)
+ .addValue("user_id2", friendId);
}
}
diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/like/JdbcLikesRepository.java b/src/main/java/ru/yandex/practicum/filmorate/repository/like/JdbcLikesRepository.java
index 85a9b6f..f309b94 100644
--- a/src/main/java/ru/yandex/practicum/filmorate/repository/like/JdbcLikesRepository.java
+++ b/src/main/java/ru/yandex/practicum/filmorate/repository/like/JdbcLikesRepository.java
@@ -14,8 +14,10 @@ public class JdbcLikesRepository implements LikesRepository {
public void addLike(long userId, long filmId) {
MapSqlParameterSource params = getParameterMap(userId, filmId);
String addLikeToFilmSql = """
- INSERT INTO likes (user_id, film_id)
- VALUES (:user_id, :film_id)""";
+ MERGE INTO likes (user_id, film_id)
+ KEY(user_id, film_id)
+ VALUES (:user_id, :film_id)
+ """;
jdbc.update(addLikeToFilmSql, params);
}
diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/review/JdbcReviewRepository.java b/src/main/java/ru/yandex/practicum/filmorate/repository/review/JdbcReviewRepository.java
new file mode 100644
index 0000000..4df544b
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/repository/review/JdbcReviewRepository.java
@@ -0,0 +1,138 @@
+package ru.yandex.practicum.filmorate.repository.review;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.jdbc.core.RowMapper;
+import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
+import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
+import org.springframework.jdbc.support.GeneratedKeyHolder;
+import org.springframework.stereotype.Repository;
+import ru.yandex.practicum.filmorate.model.Reaction;
+import ru.yandex.practicum.filmorate.model.Review;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+
+@Repository
+@RequiredArgsConstructor
+public class JdbcReviewRepository implements ReviewRepository {
+ private static final String BASE_SELECT_REVIEW_SQL = """
+ SELECT r.review_id,
+ r.content,
+ r.is_positive,
+ fr.film_id,
+ fr.user_id,
+ r.useful_rating
+ FROM reviews r
+ JOIN film_reviews fr ON r.review_id = fr.review_id
+ """;
+ private final NamedParameterJdbcOperations jdbc;
+ private final RowMapper mapper;
+
+
+ @Override
+ public Review save(Review review) {
+ GeneratedKeyHolder keyHolder = new GeneratedKeyHolder();
+ MapSqlParameterSource params = getBaseParams(review);
+ jdbc.update("""
+ INSERT INTO reviews(content, is_positive)
+ VALUES (:content, :is_Positive)
+ """, params, keyHolder, new String[]{"review_id"});
+ params.addValue("film_id", review.getFilmId());
+ params.addValue("user_id", review.getUserId());
+ review.setId(keyHolder.getKeyAs(Long.class));
+ params.addValue("review_id", review.getId());
+ jdbc.update("""
+ INSERT INTO film_reviews(film_id, user_id, review_id)
+ VALUES (:film_id, :user_id, :review_id)
+ """, params);
+ review.setUsefulRating(0L);
+ return review;
+ }
+
+ @Override
+ public void update(Review review) {
+ MapSqlParameterSource params = getBaseParams(review)
+ .addValue("review_id", review.getId());
+ jdbc.update("""
+ UPDATE reviews
+ SET content = :content, is_positive = :is_Positive
+ WHERE review_id = :review_id
+ """, params);
+ }
+
+ @Override
+ public void delete(long id) {
+ MapSqlParameterSource params = new MapSqlParameterSource("review_id", id);
+ jdbc.update("""
+ DELETE FROM reviews
+ WHERE review_id = :review_id
+ """, params);
+ }
+
+ @Override
+ public Optional findById(long id) {
+ MapSqlParameterSource params = new MapSqlParameterSource("review_id", id);
+ List list = jdbc.query(BASE_SELECT_REVIEW_SQL.concat("""
+ WHERE r.review_id = :review_id
+ ORDER BY useful_rating DESC
+ """), params, mapper);
+ return list.isEmpty() ? Optional.empty() : Optional.of(list.getFirst());
+ }
+
+ @Override
+ public Collection findAllByFilm(Long filmId, long count) {
+ MapSqlParameterSource params = new MapSqlParameterSource("film_id", filmId)
+ .addValue("count", count);
+ return jdbc.query(BASE_SELECT_REVIEW_SQL.concat("""
+ WHERE fr.film_id = COALESCE(:film_id, fr.film_id)
+ ORDER BY useful_rating DESC
+ LIMIT :count
+ """), params, mapper);
+ }
+
+ @Override
+ public void setReaction(long id, long userId, Reaction reaction) {
+ MapSqlParameterSource params = new MapSqlParameterSource("review_id", id)
+ .addValue("user_id", userId);
+ switch (reaction) {
+ case LIKE -> params.addValue("is_positive", true);
+ case DISLIKE -> params.addValue("is_positive", false);
+ }
+ jdbc.update("""
+ MERGE INTO review_likes (review_id, user_id, is_positive)
+ KEY (review_id, user_id)
+ VALUES (:review_id, :user_id, :is_positive)
+ """, params);
+ updateUsefulRating(params);
+ }
+
+ @Override
+ public void removeReaction(long id, long userId) {
+ MapSqlParameterSource params = new MapSqlParameterSource("review_id", id)
+ .addValue("user_id", userId);
+ jdbc.update("""
+ DELETE FROM review_likes
+ WHERE review_id = :review_id AND user_id = :user_id
+ """, params);
+ updateUsefulRating(params);
+ }
+
+ private MapSqlParameterSource getBaseParams(Review review) {
+ return new MapSqlParameterSource()
+ .addValue("content", review.getContent())
+ .addValue("is_Positive", review.getIsPositive());
+ }
+
+ private void updateUsefulRating(MapSqlParameterSource params) {
+ jdbc.update("""
+ UPDATE reviews r
+ SET useful_rating = COALESCE((
+ SELECT SUM(CASE WHEN is_positive THEN 1 ELSE -1 END)
+ FROM review_likes rl
+ WHERE rl.review_id = r.review_id),
+ 0)
+ WHERE r.review_id = :review_id;
+ """, params);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/review/ReviewRepository.java b/src/main/java/ru/yandex/practicum/filmorate/repository/review/ReviewRepository.java
new file mode 100644
index 0000000..cf91540
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/repository/review/ReviewRepository.java
@@ -0,0 +1,23 @@
+package ru.yandex.practicum.filmorate.repository.review;
+
+import ru.yandex.practicum.filmorate.model.Reaction;
+import ru.yandex.practicum.filmorate.model.Review;
+
+import java.util.Collection;
+import java.util.Optional;
+
+public interface ReviewRepository {
+ Review save(Review review);
+
+ void update(Review review);
+
+ void delete(long id);
+
+ Optional findById(long id);
+
+ Collection findAllByFilm(Long filmId, long count);
+
+ void setReaction(long id, long userId, Reaction reaction);
+
+ void removeReaction(long id, long userId);
+}
\ No newline at end of file
diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/review/ReviewRowMapper.java b/src/main/java/ru/yandex/practicum/filmorate/repository/review/ReviewRowMapper.java
new file mode 100644
index 0000000..870c76d
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/repository/review/ReviewRowMapper.java
@@ -0,0 +1,23 @@
+package ru.yandex.practicum.filmorate.repository.review;
+
+import org.springframework.jdbc.core.RowMapper;
+import org.springframework.stereotype.Component;
+import ru.yandex.practicum.filmorate.model.Review;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+@Component
+public class ReviewRowMapper implements RowMapper {
+ @Override
+ public Review mapRow(ResultSet rs, int rowNum) throws SQLException {
+ return Review.builder()
+ .id(rs.getLong("review_id"))
+ .content(rs.getString("content"))
+ .isPositive(rs.getBoolean("is_positive"))
+ .filmId(rs.getLong("film_id"))
+ .userId(rs.getLong("user_id"))
+ .usefulRating(rs.getLong("useful_rating"))
+ .build();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/user/JdbcUserRepository.java b/src/main/java/ru/yandex/practicum/filmorate/repository/user/JdbcUserRepository.java
index 6851f9b..79b02ee 100644
--- a/src/main/java/ru/yandex/practicum/filmorate/repository/user/JdbcUserRepository.java
+++ b/src/main/java/ru/yandex/practicum/filmorate/repository/user/JdbcUserRepository.java
@@ -76,6 +76,7 @@ public void deleteById(long id) {
MapSqlParameterSource params = new MapSqlParameterSource()
.addValue("id", id);
String deleteUserByIdSql = "DELETE FROM users WHERE user_id = :id";
+
jdbc.update(deleteUserByIdSql, params);
}
@@ -113,4 +114,22 @@ WHERE u.user_id IN (SELECT user_id2
return jdbc.query(selectCommonFriendsSql, params, rowMapper);
}
+
+ public List findSimilarFilmTasteUsers(long userId) {
+ MapSqlParameterSource params = new MapSqlParameterSource()
+ .addValue("userId", userId);
+
+ String sql = """
+ SELECT l2.user_id AS other_user,
+ COUNT(*) AS common_like_count
+ FROM likes l1
+ JOIN likes l2 ON l1.film_id = l2.film_id
+ WHERE l1.user_id = :userId
+ AND l2.user_id != :userId
+ GROUP BY l2.user_id
+ ORDER BY common_like_count DESC
+ LIMIT 5""";
+
+ return jdbc.query(sql, params, (rs, rowNum) -> rs.getLong("other_user"));
+ }
}
diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/user/UserRepository.java b/src/main/java/ru/yandex/practicum/filmorate/repository/user/UserRepository.java
index e15ae2e..ed45ac7 100644
--- a/src/main/java/ru/yandex/practicum/filmorate/repository/user/UserRepository.java
+++ b/src/main/java/ru/yandex/practicum/filmorate/repository/user/UserRepository.java
@@ -3,6 +3,7 @@
import ru.yandex.practicum.filmorate.model.User;
import java.util.Collection;
+import java.util.List;
import java.util.Optional;
public interface UserRepository {
@@ -19,4 +20,6 @@ public interface UserRepository {
Collection findAllFriends(long userId);
Collection findAllCommonFriends(long userId1, long userId2);
+
+ List findSimilarFilmTasteUsers(long userId);
}
diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java b/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java
deleted file mode 100644
index f0a790f..0000000
--- a/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package ru.yandex.practicum.filmorate.service;
-
-import ru.yandex.practicum.filmorate.dto.film.FilmDto;
-import ru.yandex.practicum.filmorate.dto.film.NewFilmRequest;
-import ru.yandex.practicum.filmorate.dto.film.UpdateFilmRequest;
-
-import java.util.Collection;
-
-public interface FilmService {
- Collection findAll();
-
- FilmDto findById(long id);
-
- FilmDto create(NewFilmRequest request);
-
- FilmDto update(UpdateFilmRequest request);
-
- void deleteById(long id);
-
- void addLike(long filmId, long userId);
-
- void removeLike(long filmId, long userId);
-
- Collection findFilmsWithTopLikes(int count);
-}
diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/director/DirectorService.java b/src/main/java/ru/yandex/practicum/filmorate/service/director/DirectorService.java
new file mode 100644
index 0000000..57be400
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/service/director/DirectorService.java
@@ -0,0 +1,19 @@
+package ru.yandex.practicum.filmorate.service.director;
+
+import ru.yandex.practicum.filmorate.dto.director.DirectorDto;
+import ru.yandex.practicum.filmorate.dto.director.NewDirectorRequest;
+import ru.yandex.practicum.filmorate.dto.director.UpdateDirectorRequest;
+
+import java.util.Collection;
+
+public interface DirectorService {
+ Collection findAll();
+
+ DirectorDto findById(long id);
+
+ DirectorDto create(NewDirectorRequest request);
+
+ DirectorDto update(UpdateDirectorRequest request);
+
+ void deleteById(long id);
+}
diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/director/DirectorServiceImpl.java b/src/main/java/ru/yandex/practicum/filmorate/service/director/DirectorServiceImpl.java
new file mode 100644
index 0000000..c5df349
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/service/director/DirectorServiceImpl.java
@@ -0,0 +1,61 @@
+package ru.yandex.practicum.filmorate.service.director;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import ru.yandex.practicum.filmorate.dto.director.DirectorDto;
+import ru.yandex.practicum.filmorate.dto.director.NewDirectorRequest;
+import ru.yandex.practicum.filmorate.dto.director.UpdateDirectorRequest;
+import ru.yandex.practicum.filmorate.exception.NotFoundException;
+import ru.yandex.practicum.filmorate.mapper.DirectorMapper;
+import ru.yandex.practicum.filmorate.model.Director;
+import ru.yandex.practicum.filmorate.repository.director.DirectorRepository;
+
+import java.util.Collection;
+
+@Service
+@Slf4j
+@RequiredArgsConstructor
+public class DirectorServiceImpl implements DirectorService {
+ private final DirectorRepository directorRepository;
+
+ @Override
+ public Collection findAll() {
+ return directorRepository.findAll()
+ .stream()
+ .map(DirectorMapper::toDirectorDto)
+ .toList();
+ }
+
+ @Override
+ public DirectorDto findById(long id) {
+ return directorRepository.findById(id)
+ .map(DirectorMapper::toDirectorDto)
+ .orElseThrow(NotFoundException.supplier("Director with id %d not found", id));
+ }
+
+ @Override
+ public DirectorDto create(NewDirectorRequest request) {
+ Director director = DirectorMapper.toDirector(request);
+ director = directorRepository.create(director);
+ log.info("Director with id {} created", director.getId());
+ return DirectorMapper.toDirectorDto(director);
+ }
+
+ @Override
+ public DirectorDto update(UpdateDirectorRequest request) {
+ Director director = directorRepository.findById(request.getId()).orElseThrow(
+ NotFoundException.supplier("Director with id %d not found", request.getId())
+ );
+ director = DirectorMapper.updateDirectorFields(director, request);
+ directorRepository.update(director);
+ log.info("Director with id {} updated", director.getId());
+ return DirectorMapper.toDirectorDto(director);
+ }
+
+ @Override
+ public void deleteById(long id) {
+ directorRepository.deleteById(id);
+ log.info("Director with id {} deleted", id);
+ }
+}
diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/feed/FeedService.java b/src/main/java/ru/yandex/practicum/filmorate/service/feed/FeedService.java
new file mode 100644
index 0000000..d9f9df3
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/service/feed/FeedService.java
@@ -0,0 +1,13 @@
+package ru.yandex.practicum.filmorate.service.feed;
+
+import ru.yandex.practicum.filmorate.dto.event.EventDto;
+import ru.yandex.practicum.filmorate.model.EventType;
+import ru.yandex.practicum.filmorate.model.Operation;
+
+import java.util.List;
+
+public interface FeedService {
+ List getUserFeed(long userId);
+
+ void addEvent(EventType eventType, Operation operation, long userId, long entityId);
+}
diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/feed/FeedServiceImpl.java b/src/main/java/ru/yandex/practicum/filmorate/service/feed/FeedServiceImpl.java
new file mode 100644
index 0000000..f941602
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/service/feed/FeedServiceImpl.java
@@ -0,0 +1,49 @@
+package ru.yandex.practicum.filmorate.service.feed;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import ru.yandex.practicum.filmorate.dto.event.EventDto;
+import ru.yandex.practicum.filmorate.exception.NotFoundException;
+import ru.yandex.practicum.filmorate.mapper.EventMapper;
+import ru.yandex.practicum.filmorate.model.Event;
+import ru.yandex.practicum.filmorate.model.EventType;
+import ru.yandex.practicum.filmorate.model.Operation;
+import ru.yandex.practicum.filmorate.repository.feed.FeedRepository;
+import ru.yandex.practicum.filmorate.repository.user.UserRepository;
+
+import java.util.List;
+
+@Service
+@Slf4j
+@RequiredArgsConstructor
+public class FeedServiceImpl implements FeedService {
+ private final FeedRepository feedRepository;
+ private final UserRepository userRepository;
+
+ @Override
+ public List getUserFeed(long userId) {
+ userRepository.findById(userId)
+ .orElseThrow(NotFoundException.supplier("User with id %d not found", userId));
+
+ List events = feedRepository.findByUserId(userId);
+ log.info("Retrieved {} events for user {}", events.size(), userId);
+ return events.stream()
+ .map(EventMapper::toEventDto)
+ .toList();
+ }
+
+ @Override
+ public void addEvent(EventType eventType, Operation operation, long userId, long entityId) {
+ Event event = Event.builder()
+ .timestamp(System.currentTimeMillis())
+ .userId(userId)
+ .eventType(eventType)
+ .operation(operation)
+ .entityId(entityId)
+ .build();
+ feedRepository.save(event);
+ log.debug("Event saved: type={}, operation={}, userId={}, entityId={}",
+ eventType, operation, userId, entityId);
+ }
+}
diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/film/FilmService.java b/src/main/java/ru/yandex/practicum/filmorate/service/film/FilmService.java
new file mode 100644
index 0000000..c175560
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/service/film/FilmService.java
@@ -0,0 +1,34 @@
+package ru.yandex.practicum.filmorate.service.film;
+
+import ru.yandex.practicum.filmorate.controller.FilmsSortBy;
+import ru.yandex.practicum.filmorate.controller.SearchFilmsBy;
+import ru.yandex.practicum.filmorate.dto.film.FilmDto;
+import ru.yandex.practicum.filmorate.dto.film.NewFilmRequest;
+import ru.yandex.practicum.filmorate.dto.film.UpdateFilmRequest;
+
+import java.util.Collection;
+import java.util.List;
+
+public interface FilmService {
+ Collection findAll();
+
+ FilmDto findById(long id);
+
+ FilmDto create(NewFilmRequest request);
+
+ FilmDto update(UpdateFilmRequest request);
+
+ void deleteById(long id);
+
+ void addLike(long filmId, long userId);
+
+ void removeLike(long filmId, long userId);
+
+ Collection findFilmsWithTopLikes(int count, Integer genreId, Integer year);
+
+ Collection findCommonFilms(long userId, long friendId);
+
+ Collection findFilmsOfDirector(long directorId, FilmsSortBy sortFilmsBy);
+
+ Collection searchFilms(String query, List searchFilmsBy);
+}
diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/FilmServiceImpl.java b/src/main/java/ru/yandex/practicum/filmorate/service/film/FilmServiceImpl.java
similarity index 63%
rename from src/main/java/ru/yandex/practicum/filmorate/service/FilmServiceImpl.java
rename to src/main/java/ru/yandex/practicum/filmorate/service/film/FilmServiceImpl.java
index e03ab81..a1f17a0 100644
--- a/src/main/java/ru/yandex/practicum/filmorate/service/FilmServiceImpl.java
+++ b/src/main/java/ru/yandex/practicum/filmorate/service/film/FilmServiceImpl.java
@@ -1,23 +1,27 @@
-package ru.yandex.practicum.filmorate.service;
+package ru.yandex.practicum.filmorate.service.film;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.experimental.FieldDefaults;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
+import ru.yandex.practicum.filmorate.controller.FilmsSortBy;
+import ru.yandex.practicum.filmorate.controller.SearchFilmsBy;
+import ru.yandex.practicum.filmorate.dto.director.DirectorDto;
import ru.yandex.practicum.filmorate.dto.film.FilmDto;
import ru.yandex.practicum.filmorate.dto.film.NewFilmRequest;
import ru.yandex.practicum.filmorate.dto.film.UpdateFilmRequest;
import ru.yandex.practicum.filmorate.dto.genre.GenreDto;
import ru.yandex.practicum.filmorate.exception.NotFoundException;
import ru.yandex.practicum.filmorate.mapper.FilmMapper;
-import ru.yandex.practicum.filmorate.model.Film;
-import ru.yandex.practicum.filmorate.model.Genre;
+import ru.yandex.practicum.filmorate.model.*;
+import ru.yandex.practicum.filmorate.repository.director.DirectorRepository;
import ru.yandex.practicum.filmorate.repository.film.FilmRepository;
import ru.yandex.practicum.filmorate.repository.genre.GenreRepository;
import ru.yandex.practicum.filmorate.repository.like.LikesRepository;
import ru.yandex.practicum.filmorate.repository.mparating.MPARatingRepository;
import ru.yandex.practicum.filmorate.repository.user.UserRepository;
+import ru.yandex.practicum.filmorate.service.feed.FeedService;
import java.util.Collection;
import java.util.List;
@@ -32,6 +36,8 @@ public class FilmServiceImpl implements FilmService {
LikesRepository likesRepository;
GenreRepository genreRepository;
MPARatingRepository mpaRepository;
+ DirectorRepository directorRepository;
+ FeedService feedService;
@Override
public Collection findAll() {
@@ -64,6 +70,17 @@ public FilmDto create(NewFilmRequest request) {
throw new NotFoundException("No such genres found");
}
+ List directorIds = request.getDirectors().stream()
+ .distinct()
+ .map(DirectorDto::id)
+ .toList();
+
+ List directors = directorRepository.getByIds(directorIds);
+
+ if (directorIds.size() != directors.size()) {
+ throw new NotFoundException("No such directors found");
+ }
+
film = filmRepository.save(film);
log.info("Film with id {} has been created", film.getId());
return FilmMapper.toFilmDto(film);
@@ -90,6 +107,20 @@ public FilmDto update(UpdateFilmRequest request) {
}
}
+ if (request.hasDirectors()) {
+
+ List directorIds = request.getDirectors().stream()
+ .distinct()
+ .map(DirectorDto::id)
+ .toList();
+
+ List directors = directorRepository.getByIds(directorIds);
+
+ if (directorIds.size() != directors.size()) {
+ throw new NotFoundException("No such directors found");
+ }
+ }
+
film = FilmMapper.updateFilmFields(film, request);
log.info("Film with id {} has been updated", film.getId());
filmRepository.update(film);
@@ -107,6 +138,7 @@ public void addLike(long filmId, long userId) {
throwIfFilmNotFound(filmId);
throwIfUserNotFound(userId);
likesRepository.addLike(userId, filmId);
+ feedService.addEvent(EventType.LIKE, Operation.ADD, userId, filmId);
log.info("Like to film with id {} has been added by user {}", filmId, userId);
}
@@ -115,17 +147,52 @@ public void removeLike(long filmId, long userId) {
throwIfFilmNotFound(filmId);
throwIfUserNotFound(userId);
likesRepository.removeLike(userId, filmId);
+ feedService.addEvent(EventType.LIKE, Operation.REMOVE, userId, filmId);
log.info("Like to film with id {} has been removed by user {}", filmId, userId);
}
@Override
- public Collection findFilmsWithTopLikes(int count) {
- return filmRepository.findTopPopularFilms(count)
+ public Collection findFilmsWithTopLikes(int count, Integer genreId, Integer year) {
+ return filmRepository.findTopPopularFilms(count, genreId, year)
.stream()
.map(FilmMapper::toFilmDto)
.toList();
}
+ @Override
+ public Collection findCommonFilms(long userId, long friendId) {
+ throwIfUserNotFound(userId);
+ throwIfUserNotFound(friendId);
+
+ return filmRepository.findCommonFilms(userId, friendId)
+ .stream()
+ .map(FilmMapper::toFilmDto)
+ .toList();
+ }
+
+ @Override
+ public Collection findFilmsOfDirector(long directorId, FilmsSortBy sortFilmsBy) {
+ throwIfDirectorNotFound(directorId);
+
+ return filmRepository.findFilmsOfDirector(directorId, sortFilmsBy)
+ .stream()
+ .map(FilmMapper::toFilmDto)
+ .toList();
+ }
+
+ @Override
+ public Collection searchFilms(String query, List searchFilmsBy) {
+ return filmRepository.searchFilms(query, searchFilmsBy)
+ .stream()
+ .map(FilmMapper::toFilmDto)
+ .toList();
+ }
+
+ private void throwIfDirectorNotFound(long directorId) {
+ directorRepository.findById(directorId)
+ .orElseThrow(NotFoundException.supplier("Director with id %d not found", directorId));
+ }
+
private void throwIfUserNotFound(long userId) {
userRepository.findById(userId)
.orElseThrow(NotFoundException.supplier("User with id %d not found", userId));
diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/GenreService.java b/src/main/java/ru/yandex/practicum/filmorate/service/genre/GenreService.java
similarity index 78%
rename from src/main/java/ru/yandex/practicum/filmorate/service/GenreService.java
rename to src/main/java/ru/yandex/practicum/filmorate/service/genre/GenreService.java
index 04be5dc..182990c 100644
--- a/src/main/java/ru/yandex/practicum/filmorate/service/GenreService.java
+++ b/src/main/java/ru/yandex/practicum/filmorate/service/genre/GenreService.java
@@ -1,4 +1,4 @@
-package ru.yandex.practicum.filmorate.service;
+package ru.yandex.practicum.filmorate.service.genre;
import ru.yandex.practicum.filmorate.dto.genre.GenreDto;
diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/GenreServiceImpl.java b/src/main/java/ru/yandex/practicum/filmorate/service/genre/GenreServiceImpl.java
similarity index 94%
rename from src/main/java/ru/yandex/practicum/filmorate/service/GenreServiceImpl.java
rename to src/main/java/ru/yandex/practicum/filmorate/service/genre/GenreServiceImpl.java
index 23f435e..848c11e 100644
--- a/src/main/java/ru/yandex/practicum/filmorate/service/GenreServiceImpl.java
+++ b/src/main/java/ru/yandex/practicum/filmorate/service/genre/GenreServiceImpl.java
@@ -1,4 +1,4 @@
-package ru.yandex.practicum.filmorate.service;
+package ru.yandex.practicum.filmorate.service.genre;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/MPARatingService.java b/src/main/java/ru/yandex/practicum/filmorate/service/mparating/MPARatingService.java
similarity index 79%
rename from src/main/java/ru/yandex/practicum/filmorate/service/MPARatingService.java
rename to src/main/java/ru/yandex/practicum/filmorate/service/mparating/MPARatingService.java
index 675edc1..68a73d6 100644
--- a/src/main/java/ru/yandex/practicum/filmorate/service/MPARatingService.java
+++ b/src/main/java/ru/yandex/practicum/filmorate/service/mparating/MPARatingService.java
@@ -1,4 +1,4 @@
-package ru.yandex.practicum.filmorate.service;
+package ru.yandex.practicum.filmorate.service.mparating;
import ru.yandex.practicum.filmorate.dto.mparating.MPARatingDto;
diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/MPARatingServiceImpl.java b/src/main/java/ru/yandex/practicum/filmorate/service/mparating/MPARatingServiceImpl.java
similarity index 94%
rename from src/main/java/ru/yandex/practicum/filmorate/service/MPARatingServiceImpl.java
rename to src/main/java/ru/yandex/practicum/filmorate/service/mparating/MPARatingServiceImpl.java
index 85d5140..d9d80d5 100644
--- a/src/main/java/ru/yandex/practicum/filmorate/service/MPARatingServiceImpl.java
+++ b/src/main/java/ru/yandex/practicum/filmorate/service/mparating/MPARatingServiceImpl.java
@@ -1,4 +1,4 @@
-package ru.yandex.practicum.filmorate.service;
+package ru.yandex.practicum.filmorate.service.mparating;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/recommendation/RecommendationService.java b/src/main/java/ru/yandex/practicum/filmorate/service/recommendation/RecommendationService.java
new file mode 100644
index 0000000..2b048b6
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/service/recommendation/RecommendationService.java
@@ -0,0 +1,9 @@
+package ru.yandex.practicum.filmorate.service.recommendation;
+
+import ru.yandex.practicum.filmorate.dto.film.FilmDto;
+
+import java.util.Collection;
+
+public interface RecommendationService {
+ Collection findFilmRecommendations(long userId);
+}
diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/recommendation/RecommendationServiceImpl.java b/src/main/java/ru/yandex/practicum/filmorate/service/recommendation/RecommendationServiceImpl.java
new file mode 100644
index 0000000..26d9c8e
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/service/recommendation/RecommendationServiceImpl.java
@@ -0,0 +1,39 @@
+package ru.yandex.practicum.filmorate.service.recommendation;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import ru.yandex.practicum.filmorate.dto.film.FilmDto;
+import ru.yandex.practicum.filmorate.exception.NotFoundException;
+import ru.yandex.practicum.filmorate.mapper.FilmMapper;
+import ru.yandex.practicum.filmorate.repository.film.FilmRepository;
+import ru.yandex.practicum.filmorate.repository.user.UserRepository;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+@Service
+@Slf4j
+@RequiredArgsConstructor
+public class RecommendationServiceImpl implements RecommendationService {
+ private final UserRepository userRepository;
+ private final FilmRepository filmRepository;
+
+ @Override
+ public Collection findFilmRecommendations(long userId) {
+ userRepository.findById(userId)
+ .orElseThrow(NotFoundException.supplier("User with id %d not found", userId));
+
+ List similarUsers = userRepository.findSimilarFilmTasteUsers(userId);
+
+ if (similarUsers.isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ return filmRepository.findFilmRecommendations(userId, similarUsers)
+ .stream()
+ .map(FilmMapper::toFilmDto)
+ .toList();
+ }
+}
diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/review/ReviewService.java b/src/main/java/ru/yandex/practicum/filmorate/service/review/ReviewService.java
new file mode 100644
index 0000000..10a346a
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/service/review/ReviewService.java
@@ -0,0 +1,27 @@
+package ru.yandex.practicum.filmorate.service.review;
+
+import ru.yandex.practicum.filmorate.dto.review.NewReviewRequest;
+import ru.yandex.practicum.filmorate.dto.review.ReviewDto;
+import ru.yandex.practicum.filmorate.dto.review.UpdateReviewRequest;
+import ru.yandex.practicum.filmorate.model.Reaction;
+
+import java.util.Collection;
+
+public interface ReviewService {
+
+ ReviewDto create(NewReviewRequest request);
+
+ ReviewDto update(UpdateReviewRequest request);
+
+ void delete(long id);
+
+ ReviewDto findById(long id);
+
+ Collection findAllByFilm(Long filmId, long count);
+
+ void setLike(long id, long userId);
+
+ void setDislike(long id, long userId);
+
+ void removeReaction(long id, long userId, Reaction reaction);
+}
\ No newline at end of file
diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/review/ReviewServiceImpl.java b/src/main/java/ru/yandex/practicum/filmorate/service/review/ReviewServiceImpl.java
new file mode 100644
index 0000000..701cce3
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/service/review/ReviewServiceImpl.java
@@ -0,0 +1,115 @@
+package ru.yandex.practicum.filmorate.service.review;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import ru.yandex.practicum.filmorate.dto.review.*;
+import ru.yandex.practicum.filmorate.exception.NotFoundException;
+import ru.yandex.practicum.filmorate.mapper.ReviewMapper;
+import ru.yandex.practicum.filmorate.model.EventType;
+import ru.yandex.practicum.filmorate.model.Operation;
+import ru.yandex.practicum.filmorate.model.Reaction;
+import ru.yandex.practicum.filmorate.model.Review;
+import ru.yandex.practicum.filmorate.repository.film.FilmRepository;
+import ru.yandex.practicum.filmorate.repository.review.ReviewRepository;
+import ru.yandex.practicum.filmorate.repository.user.UserRepository;
+import ru.yandex.practicum.filmorate.service.feed.FeedService;
+
+import java.util.Collection;
+
+@Service
+@Slf4j
+@RequiredArgsConstructor
+public class ReviewServiceImpl implements ReviewService {
+ private final ReviewRepository reviewRepository;
+ private final UserRepository userRepository;
+ private final FilmRepository filmRepository;
+ private final FeedService feedService;
+
+ @Override
+ public ReviewDto create(NewReviewRequest request) {
+ throwIfUserNotFound(request.getUserId());
+ throwIfFilmNotFound(request.getFilmId());
+ Review review = ReviewMapper.toReview(request);
+ review = reviewRepository.save(review);
+ feedService.addEvent(EventType.REVIEW, Operation.ADD, request.getUserId(), review.getId());
+ log.info("Review with reviewId {} has been created", review.getId());
+ return ReviewMapper.toDto(review);
+ }
+
+ @Override
+ public ReviewDto update(UpdateReviewRequest request) {
+ Review review = getReviewOrThrow(request.getReviewId());
+ review = ReviewMapper.updateReviewFields(review, request);
+ feedService.addEvent(EventType.REVIEW, Operation.UPDATE, review.getUserId(), review.getId());
+ log.info("Review with reviewId {} has been updated", review.getId());
+ reviewRepository.update(review);
+ return ReviewMapper.toDto(review);
+ }
+
+ @Override
+ public void delete(long id) {
+ Review review = getReviewOrThrow(id);
+ feedService.addEvent(EventType.REVIEW, Operation.REMOVE, review.getUserId(), id);
+ log.info("Review with reviewId {} has been deleted", id);
+ reviewRepository.delete(id);
+ }
+
+ @Override
+ public ReviewDto findById(long id) {
+ return ReviewMapper.toDto(getReviewOrThrow(id));
+ }
+
+ @Override
+ public Collection findAllByFilm(Long filmId, long count) {
+ if (filmId != null) {
+ throwIfFilmNotFound(filmId);
+ }
+
+ return reviewRepository.findAllByFilm(filmId, count).stream()
+ .map(ReviewMapper::toDto)
+ .toList();
+ }
+
+ @Override
+ public void setLike(long id, long userId) {
+ getReviewOrThrow(id);
+ throwIfUserNotFound(userId);
+ log.info("Like to review {} has been added by user {}", id, userId);
+ reviewRepository.setReaction(id, userId, Reaction.LIKE);
+ }
+
+ @Override
+ public void setDislike(long id, long userId) {
+ getReviewOrThrow(id);
+ throwIfUserNotFound(userId);
+ log.info("Dislike to review {} has been added by user {}", id, userId);
+ reviewRepository.setReaction(id, userId, Reaction.DISLIKE);
+ }
+
+ @Override
+ public void removeReaction(long id, long userId, Reaction reaction) {
+ getReviewOrThrow(id);
+ throwIfUserNotFound(userId);
+ switch (reaction) {
+ case LIKE -> log.info("Like to review {} has been removed by user {}", id, userId);
+ case DISLIKE -> log.info("Dislike to review {} has been removed by user {}", id, userId);
+ }
+ reviewRepository.removeReaction(id, userId);
+ }
+
+ private Review getReviewOrThrow(long id) {
+ return reviewRepository.findById(id)
+ .orElseThrow(NotFoundException.supplier("Review with reviewId %d not found", id));
+ }
+
+ private void throwIfUserNotFound(long id) {
+ userRepository.findById(id)
+ .orElseThrow(NotFoundException.supplier("User with userId %d not found", id));
+ }
+
+ private void throwIfFilmNotFound(long id) {
+ filmRepository.findById(id)
+ .orElseThrow(NotFoundException.supplier("Film with filmId %d not found", id));
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java b/src/main/java/ru/yandex/practicum/filmorate/service/user/UserService.java
similarity index 92%
rename from src/main/java/ru/yandex/practicum/filmorate/service/UserService.java
rename to src/main/java/ru/yandex/practicum/filmorate/service/user/UserService.java
index 5163030..fc35bd8 100644
--- a/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java
+++ b/src/main/java/ru/yandex/practicum/filmorate/service/user/UserService.java
@@ -1,4 +1,4 @@
-package ru.yandex.practicum.filmorate.service;
+package ru.yandex.practicum.filmorate.service.user;
import ru.yandex.practicum.filmorate.dto.user.NewUserRequest;
import ru.yandex.practicum.filmorate.dto.user.UpdateUserRequest;
diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/UserServiceImpl.java b/src/main/java/ru/yandex/practicum/filmorate/service/user/UserServiceImpl.java
similarity index 87%
rename from src/main/java/ru/yandex/practicum/filmorate/service/UserServiceImpl.java
rename to src/main/java/ru/yandex/practicum/filmorate/service/user/UserServiceImpl.java
index f7baaaf..f199adf 100644
--- a/src/main/java/ru/yandex/practicum/filmorate/service/UserServiceImpl.java
+++ b/src/main/java/ru/yandex/practicum/filmorate/service/user/UserServiceImpl.java
@@ -1,4 +1,4 @@
-package ru.yandex.practicum.filmorate.service;
+package ru.yandex.practicum.filmorate.service.user;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -8,9 +8,12 @@
import ru.yandex.practicum.filmorate.dto.user.UserDto;
import ru.yandex.practicum.filmorate.exception.NotFoundException;
import ru.yandex.practicum.filmorate.mapper.UserMapper;
+import ru.yandex.practicum.filmorate.model.EventType;
+import ru.yandex.practicum.filmorate.model.Operation;
import ru.yandex.practicum.filmorate.model.User;
import ru.yandex.practicum.filmorate.repository.friendship.FriendshipsRepository;
import ru.yandex.practicum.filmorate.repository.user.UserRepository;
+import ru.yandex.practicum.filmorate.service.feed.FeedService;
import java.util.Collection;
@@ -20,6 +23,7 @@
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
private final FriendshipsRepository friendshipsRepository;
+ private final FeedService feedService;
@Override
public Collection findAll() {
@@ -38,7 +42,7 @@ public UserDto findById(Long id) {
@Override
public UserDto create(NewUserRequest request) {
- if (request.getName() == null) {
+ if (request.getName() == null || request.getName().isEmpty()) {
request.setName(request.getLogin());
}
User user = UserMapper.mapToUser(request);
@@ -68,8 +72,8 @@ public void deleteById(long id) {
public void addFriend(long userId, long friendId) {
throwIfUserNotFound(userId);
throwIfUserNotFound(friendId);
-
friendshipsRepository.addFriendship(userId, friendId);
+ feedService.addEvent(EventType.FRIEND, Operation.ADD, userId, friendId);
log.info("User with id {} added user with id {} as friend", userId, friendId);
}
@@ -78,6 +82,7 @@ public void removeFriend(long userId, long friendId) {
throwIfUserNotFound(userId);
throwIfUserNotFound(friendId);
friendshipsRepository.removeFriendship(userId, friendId);
+ feedService.addEvent(EventType.FRIEND, Operation.REMOVE, userId, friendId);
log.info("User with id {} removed user with id {} from friends", userId, friendId);
}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index a840ff7..fc2688c 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -1,8 +1,8 @@
-logging.level.org.zalando.logbook=Trace
+logging.level.org.zalando.logbook=INFO
spring.sql.init.mode=ALWAYS
spring.datasource.url=jdbc:h2:file:./db/filmorate
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
-spring.datasource.password=password
\ No newline at end of file
+spring.datasource.password=password
diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql
index 08c651a..0dc4b91 100644
--- a/src/main/resources/schema.sql
+++ b/src/main/resources/schema.sql
@@ -19,17 +19,32 @@ CREATE TABLE IF NOT EXISTS mpa_ratings
name varchar(20) NOT NULL UNIQUE
);
+CREATE TABLE IF NOT EXISTS directors
+(
+ director_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
+ name varchar(120) NOT NULL UNIQUE
+);
+
CREATE TABLE IF NOT EXISTS films
(
film_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name varchar(120) NOT NULL,
description varchar(200) NOT NULL,
release_date date NOT NULL CHECK (release_date >= '1895-12-28'),
- duration_in_minutes BIGINT NOT NULL CHECK (duration_in_minutes > 0),
- mpa_id BIGINT NOT NULL,
+ duration_in_minutes BIGINT NOT NULL CHECK (duration_in_minutes > 0),
+ mpa_id BIGINT NOT NULL,
FOREIGN KEY (mpa_id) REFERENCES mpa_ratings (mpa_id)
);
+CREATE TABLE IF NOT EXISTS film_directors
+(
+ film_id BIGINT NOT NULL,
+ director_id BIGINT NOT NULL,
+ PRIMARY KEY (film_id, director_id),
+ FOREIGN KEY (film_id) REFERENCES films (film_id) ON DELETE CASCADE,
+ FOREIGN KEY (director_id) REFERENCES directors (director_id) ON DELETE CASCADE
+);
+
CREATE TABLE IF NOT EXISTS film_genres
(
film_id BIGINT NOT NULL,
@@ -50,11 +65,50 @@ CREATE TABLE IF NOT EXISTS likes
CREATE TABLE IF NOT EXISTS friendships
(
- user_id1 BIGINT NOT NULL,
- user_id2 BIGINT NOT NULL,
+ user_id1 BIGINT NOT NULL,
+ user_id2 BIGINT NOT NULL,
status ENUM ('pending', 'accepted') NOT NULL DEFAULT 'pending',
PRIMARY KEY (user_id1, user_id2),
FOREIGN KEY (user_id1) REFERENCES users (user_id) ON DELETE CASCADE,
FOREIGN KEY (user_id2) REFERENCES users (user_id) ON DELETE CASCADE,
CHECK (user_id1 <> user_id2)
-);
\ No newline at end of file
+);
+
+CREATE TABLE IF NOT EXISTS reviews
+(
+ review_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
+ content VARCHAR(500) NOT NULL,
+ is_positive BOOL NOT NULL,
+ useful_rating BIGINT NOT NULL DEFAULT 0
+);
+
+CREATE TABLE IF NOT EXISTS film_reviews
+(
+ review_id BIGINT NOT NULL UNIQUE,
+ film_id BIGINT NOT NULL,
+ user_id BIGINT NOT NULL,
+ PRIMARY KEY (film_id, user_id),
+ FOREIGN KEY (film_id) REFERENCES films (film_id) ON DELETE CASCADE,
+ FOREIGN KEY (user_id) REFERENCES users (user_id) ON DELETE CASCADE,
+ FOREIGN KEY (review_id) REFERENCES reviews (review_id) ON DELETE CASCADE
+);
+
+CREATE TABLE IF NOT EXISTS review_likes
+(
+ review_id BIGINT NOT NULL,
+ user_id BIGINT NOT NULL,
+ is_positive BOOL NOT NULL,
+ PRIMARY KEY (review_id, user_id),
+ FOREIGN KEY (review_id) REFERENCES reviews (review_id) ON DELETE CASCADE,
+ FOREIGN KEY (user_id) REFERENCES users (user_id) ON DELETE CASCADE
+);
+
+CREATE TABLE IF NOT EXISTS feed_events (
+ event_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
+ created_at BIGINT NOT NULL,
+ user_id BIGINT NOT NULL,
+ event_type ENUM('LIKE', 'REVIEW', 'FRIEND') NOT NULL,
+ operation ENUM('REMOVE', 'ADD', 'UPDATE') NOT NULL,
+ entity_id BIGINT NOT NULL,
+ FOREIGN KEY (user_id) REFERENCES users (user_id) ON DELETE CASCADE
+);
diff --git a/src/test/java/ru/yandex/practicum/filmorate/repository/JdbcFilmRepositoryTest.java b/src/test/java/ru/yandex/practicum/filmorate/repository/JdbcFilmRepositoryTest.java
index 4f6b5dd..d4ade53 100644
--- a/src/test/java/ru/yandex/practicum/filmorate/repository/JdbcFilmRepositoryTest.java
+++ b/src/test/java/ru/yandex/practicum/filmorate/repository/JdbcFilmRepositoryTest.java
@@ -7,10 +7,8 @@
import org.springframework.context.annotation.Import;
import ru.yandex.practicum.filmorate.model.Film;
import ru.yandex.practicum.filmorate.model.MPARating;
-import ru.yandex.practicum.filmorate.repository.film.FilmResultSetExtractor;
import ru.yandex.practicum.filmorate.repository.film.FilmRowMapper;
import ru.yandex.practicum.filmorate.repository.film.JdbcFilmRepository;
-import ru.yandex.practicum.filmorate.repository.genre.GenreRowMapper;
import ru.yandex.practicum.filmorate.util.TestFilmUtils;
import java.time.LocalDate;
@@ -20,7 +18,7 @@
import static org.assertj.core.api.Assertions.assertThat;
@JdbcTest
-@Import({JdbcFilmRepository.class, FilmRowMapper.class, GenreRowMapper.class, FilmResultSetExtractor.class})
+@Import({JdbcFilmRepository.class, FilmRowMapper.class})
public class JdbcFilmRepositoryTest {
@Autowired
private JdbcFilmRepository filmRepository;