Skip to content
323 changes: 244 additions & 79 deletions README.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package ru.yandex.practicum.filmorate.controller;

import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import ru.yandex.practicum.filmorate.model.Director;
import ru.yandex.practicum.filmorate.service.DirectorService;

import java.util.List;

@Slf4j
@RestController
@RequestMapping("/directors")
public class DirectorController {

private final DirectorService directorService;

@Autowired
public DirectorController(DirectorService directorService) {
this.directorService = directorService;
}

@GetMapping
public List<Director> getDirectors() {
log.info("GET /directors");
return directorService.getAllDirectors();
}

@GetMapping("/{id}")
public Director getDirectorById(@PathVariable long id) {
log.info("GET /directors/{}", id);
return directorService.getDirectorById(id);
}

@PostMapping
public Director createDirector(@Valid @RequestBody Director director) {
log.info("POST /directors, body:{}", director);
return directorService.addDirector(director);
}

@PutMapping
public Director updateDirector(@Valid @RequestBody Director director) {
log.info("PUT /directors, body:{}", director);
return directorService.updateDirector(director);
}

@DeleteMapping("/{id}")
public void deleteDirector(@PathVariable long id) {
log.info("DELETE /directors/{}", id);
directorService.deleteDirector(id);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import ru.yandex.practicum.filmorate.service.FilmService;

import java.util.Collection;
import java.util.List;

@RestController
@RequestMapping("/films")
Expand Down Expand Up @@ -45,6 +46,12 @@ public Film update(@Valid @RequestBody Film film) {
return filmService.update(film);
}

@DeleteMapping("/{id}")
public void delete(@PathVariable long id) {
log.info("DELETE /films/{}", id);
filmService.delete(id);
}

@PutMapping("/{id}/like/{userId}")
public Film addLike(@PathVariable long id, @PathVariable long userId) {
log.info("PUT /films/{}/like/{}", id, userId);
Expand All @@ -59,8 +66,30 @@ public Film removeLike(@PathVariable long id, @PathVariable long userId) {

@GetMapping("/popular")
public Collection<Film> getPopular(
@RequestParam(defaultValue = "10") int count) {
log.info("GET /films/popular?count={}", count);
return filmService.getPopular(count);
@RequestParam(defaultValue = "10") int count,
@RequestParam(required = false) Integer genreId,
@RequestParam(required = false) Integer year) {
log.info("GET /films/popular?count={}&genreId={}&year={}", count, genreId, year);
return filmService.getPopular(count, genreId, year);
}

@GetMapping("/common")
public Collection<Film> getCommon(@RequestParam long userId, @RequestParam long friendId) {
log.info("GET /films/common?userId={}&friendId={}", userId, friendId);
return filmService.getCommon(userId, friendId);
}

@GetMapping("/director/{directorId}")
public Collection<Film> getFilmsByDirector(@PathVariable long directorId,
@RequestParam(defaultValue = "likes") String sortBy) {
log.info("GET /films/director/{}?sortBy={}", directorId, sortBy);
return filmService.findByDirectorSorted(directorId, sortBy);
}

@GetMapping("/search")
public Collection<Film> searchFilms(@RequestParam String query,
@RequestParam List<String> by) {
log.info("GET /films/search?query={}&by={}", query, by);
return filmService.searchFilms(query, by);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package ru.yandex.practicum.filmorate.controller;

import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import ru.yandex.practicum.filmorate.model.Review;
import ru.yandex.practicum.filmorate.service.ReviewService;

import java.util.Collection;

@RestController
@RequestMapping("/reviews")
@Slf4j
public class ReviewController {

private final ReviewService reviewService;

@Autowired
public ReviewController(ReviewService reviewService) {
this.reviewService = reviewService;
}

@PostMapping
public Review create(@Valid @RequestBody Review review) {
log.info("POST /reviews : userId={}, filmId={}", review.getUserId(), review.getFilmId());
return reviewService.add(review);
}

@PutMapping
public Review update(@Valid @RequestBody Review review) {
log.info("PUT /reviews : id={}", review.getReviewId());
return reviewService.update(review);
}

@DeleteMapping("/{id}")
public void delete(@PathVariable long id) {
log.info("DELETE /reviews/{}", id);
reviewService.delete(id);
}

@GetMapping("/{id}")
public Review findById(@PathVariable long id) {
log.info("GET /reviews/{}", id);
return reviewService.findById(id);
}

@GetMapping
public Collection<Review> findMostUseful(
@RequestParam(required = false) Long filmId,
@RequestParam(defaultValue = "10") int count) {
log.info("GET /reviews?filmId={}&count={}", filmId, count);
return reviewService.findMostUseful(filmId, count);
}

@PutMapping("/{id}/like/{userId}")
public void addLike(@PathVariable long id, @PathVariable long userId) {
log.info("PUT /reviews/{}/like/{}", id, userId);
reviewService.addLike(id, userId);
}

@PutMapping("/{id}/dislike/{userId}")
public void addDislike(@PathVariable long id, @PathVariable long userId) {
log.info("PUT /reviews/{}/dislike/{}", id, userId);
reviewService.addDislike(id, userId);
}

@DeleteMapping("/{id}/like/{userId}")
public void removeLike(@PathVariable long id, @PathVariable long userId) {
log.info("DELETE /reviews/{}/like/{}", id, userId);
reviewService.removeLike(id, userId);
}

@DeleteMapping("/{id}/dislike/{userId}")
public void removeDislike(@PathVariable long id, @PathVariable long userId) {
log.info("DELETE /reviews/{}/dislike/{}", id, userId);
reviewService.removeDislike(id, userId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import ru.yandex.practicum.filmorate.model.Event;
import ru.yandex.practicum.filmorate.model.Film;
import ru.yandex.practicum.filmorate.model.User;
import ru.yandex.practicum.filmorate.service.UserService;

import java.util.Collection;
import java.util.List;

@RestController
@RequestMapping("/users")
Expand Down Expand Up @@ -45,6 +48,12 @@ public User update(@Valid @RequestBody User user) {
return userService.update(user);
}

@DeleteMapping("/{id}")
public void delete(@PathVariable long id) {
log.info("DELETE /users/{}", id);
userService.delete(id);
}

@PutMapping("/{id}/friends/{friendId}")
public User addFriend(@PathVariable long id, @PathVariable long friendId) {
log.info("PUT /users/{}/friends/{}", id, friendId);
Expand All @@ -70,4 +79,16 @@ public Collection<User> getCommonFriends(
log.info("GET /users/{}/friends/common/{}", id, otherId);
return userService.getCommonFriends(id, otherId);
}

@GetMapping("/{id}/recommendations")
public Collection<Film> getRecommendations(@PathVariable long id) {
log.info("GET /users/{}/recommendations", id);
return userService.getRecommendations(id);
}

@GetMapping("/{id}/feed")
public List<Event> getFeed(@PathVariable long id) {
log.info("GET /users/{}/feed", id);
return userService.getFeed(id);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,19 @@

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.servlet.resource.NoResourceFoundException;

import java.util.Map;
import java.util.stream.Collectors;

@RestControllerAdvice
@Slf4j
Expand All @@ -27,18 +34,70 @@ public Map<String, String> handleNotFoundException(NotFoundException e) {
return Map.of("error", e.getMessage());
}

// Нарушены аннотации валидации (@NotBlank, @Positive и т.п.): в ответе — поле и что с ним не так
@ExceptionHandler
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Map<String, String> handleMethodArgumentNotValidException(
MethodArgumentNotValidException e) {
public Map<String, String> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
String message = e.getBindingResult().getFieldErrors().stream()
.map(error -> error.getField() + ": " + error.getDefaultMessage())
.collect(Collectors.joining("; "));
log.warn("400: {}", message);
return Map.of("error", message);
}

// Нет обязательного параметра, например GET /films/search без by
@ExceptionHandler
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Map<String, String> handleMissingParameter(MissingServletRequestParameterException e) {
log.warn("400: {}", e.getMessage());
return Map.of("error", e.getMessage());
return Map.of("error", "Не указан обязательный параметр '" + e.getParameterName() + "'");
}

// Параметр не того типа, например GET /films/abc вместо числового id
@ExceptionHandler
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Map<String, String> handleTypeMismatch(MethodArgumentTypeMismatchException e) {
log.warn("400: {}", e.getMessage());
return Map.of("error", "Некорректное значение параметра '" + e.getName() + "': " + e.getValue());
}

// Тело запроса не читается как JSON нужной структуры
@ExceptionHandler
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Map<String, String> handleNotReadableBody(HttpMessageNotReadableException e) {
log.warn("400: {}", e.getMessage());
return Map.of("error", "Некорректное тело запроса");
}

// Запрос на адрес, которого нет в API
@ExceptionHandler
@ResponseStatus(HttpStatus.NOT_FOUND)
public Map<String, String> handleNoResource(NoResourceFoundException e) {
log.warn("404: {}", e.getMessage());
return Map.of("error", "Адрес /" + e.getResourcePath() + " не найден");
}

// Адрес есть, но с другим HTTP-методом, например POST /films/1
@ExceptionHandler
@ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
public Map<String, String> handleMethodNotSupported(HttpRequestMethodNotSupportedException e) {
log.warn("405: {}", e.getMessage());
return Map.of("error", "Метод " + e.getMethod() + " для этого адреса не поддерживается");
}

// Тело пришло не в JSON, например с Content-Type: text/plain
@ExceptionHandler
@ResponseStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
public Map<String, String> handleMediaTypeNotSupported(HttpMediaTypeNotSupportedException e) {
log.warn("415: {}", e.getMessage());
return Map.of("error", "Тело запроса должно быть в формате JSON");
}

// Непредвиденная ошибка: подробности только в лог, клиенту — общее сообщение без SQL и стектрейсов
@ExceptionHandler
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Map<String, String> handleException(Exception e) {
log.error("500: {}", e.getMessage(), e);
return Map.of("error", e.getMessage());
return Map.of("error", "Внутренняя ошибка сервера");
}
}
}
15 changes: 15 additions & 0 deletions src/main/java/ru/yandex/practicum/filmorate/model/Director.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package ru.yandex.practicum.filmorate.model;

import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.EqualsAndHashCode;

// Равенство по id, как у Genre и Mpa: дубль режиссёра в запросе схлопнется в один
@Data
@EqualsAndHashCode(of = "id")
public class Director {
private Long id;

@NotBlank(message = "Имя режиссёра не может быть пустым")
private String name;
}
13 changes: 13 additions & 0 deletions src/main/java/ru/yandex/practicum/filmorate/model/Event.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package ru.yandex.practicum.filmorate.model;

import lombok.Data;

@Data
public class Event {
private Long eventId;
private Long timestamp;
private Long userId;
private EventType eventType;
private Operation operation;
private Long entityId;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package ru.yandex.practicum.filmorate.model;

public enum EventType {
LIKE,
REVIEW,
FRIEND
}
Loading
Loading