diff --git a/pom.xml b/pom.xml index 2db888c..1546b4a 100644 --- a/pom.xml +++ b/pom.xml @@ -1,247 +1,270 @@ - 4.0.0 - - org.springframework.boot - spring-boot-starter-parent - 3.3.2 - - + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.3.2 + + - ru.practicum - shareit - 0.0.1-SNAPSHOT + ru.practicum + shareit + 0.0.1-SNAPSHOT - ShareIt + ShareIt - - 21 - + + 21 + - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-starter-actuator - - - org.springframework.boot - spring-boot-configuration-processor - true - + + + org.springframework.boot + spring-boot-starter-web + - - org.postgresql - postgresql - runtime - + + org.springframework.boot + spring-boot-starter-actuator + - - org.projectlombok - lombok - true - + + org.springframework.boot + spring-boot-configuration-processor + true + - - com.h2database - h2 - test - - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.boot - spring-boot-starter-validation - - + + org.postgresql + postgresql + runtime + - - - - src/main/resources - true - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - org.projectlombok - lombok - - - - - - - - - org.apache.maven.plugins - maven-surefire-plugin - - - test - - - - - org.apache.maven.plugins - maven-checkstyle-plugin - 3.1.2 - - checkstyle.xml - true - true - true - - - - - check - - compile - - - - - com.puppycrawl.tools - checkstyle - 10.3 - - - - - com.github.spotbugs - spotbugs-maven-plugin - 4.8.5.0 - - Max - High - - - - - check - - - - - - org.jacoco - jacoco-maven-plugin - 0.8.12 - - file - - - - jacoco-initialize - - prepare-agent - - - - jacoco-check - - check - - - - - BUNDLE - - - INSTRUCTION - COVEREDRATIO - 0.01 - - - LINE - COVEREDRATIO - 0.9 - - - BRANCH - COVEREDRATIO - 0.6 - - - COMPLEXITY - COVEREDRATIO - 0.6 - - - METHOD - COVEREDRATIO - 0.7 - - - CLASS - MISSEDCOUNT - 1 - - - - - - - - jacoco-report - test - - report - - - - - - - - - - check - - - - org.apache.maven.plugins - maven-checkstyle-plugin - - - com.github.spotbugs - spotbugs-maven-plugin - - - - - - - com.github.spotbugs - spotbugs-maven-plugin - - - - - - coverage - - - - org.jacoco - jacoco-maven-plugin - - - - - + + org.projectlombok + lombok + 1.18.38 + provided + - + + com.h2database + h2 + test + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + + + + src/main/resources + true + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.projectlombok + lombok + 1.18.38 + + + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + test + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.1.2 + + checkstyle.xml + true + true + true + + + + + check + + compile + + + + + com.puppycrawl.tools + checkstyle + 10.3 + + + + + com.github.spotbugs + spotbugs-maven-plugin + 4.8.5.0 + + Max + High + + + + + check + + + + + + org.jacoco + jacoco-maven-plugin + 0.8.12 + + file + + + + jacoco-initialize + + prepare-agent + + + + jacoco-check + + check + + + + + BUNDLE + + + INSTRUCTION + COVEREDRATIO + 0.01 + + + LINE + COVEREDRATIO + 0.9 + + + BRANCH + COVEREDRATIO + 0.6 + + + COMPLEXITY + COVEREDRATIO + 0.6 + + + METHOD + COVEREDRATIO + 0.7 + + + CLASS + MISSEDCOUNT + 1 + + + + + + + + jacoco-report + test + + report + + + + + + + + + + check + + + + org.apache.maven.plugins + maven-checkstyle-plugin + + + com.github.spotbugs + spotbugs-maven-plugin + + + + + + + com.github.spotbugs + spotbugs-maven-plugin + + + + + + coverage + + + + org.jacoco + jacoco-maven-plugin + + + + + + + \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/booking/BookingController.java b/src/main/java/ru/practicum/shareit/booking/BookingController.java new file mode 100644 index 0000000..d107637 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/BookingController.java @@ -0,0 +1,60 @@ +package ru.practicum.shareit.booking; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; +import ru.practicum.shareit.booking.dto.BookingDto; +import ru.practicum.shareit.booking.dto.BookingNewDto; +import ru.practicum.shareit.booking.model.BookingState; + +import java.util.Collection; + +/** + * TODO Sprint add-bookings. + */ +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping(path = "/bookings") +public class BookingController { + + private final BookingService bookingService; + + @GetMapping("/{bookingId}") + public BookingDto findById(@RequestHeader("X-Sharer-User-Id") Long userId, + @PathVariable Long bookingId) { + log.info("Запрос на получение бронирования с Id:{} от пользователя с Id:{}", bookingId, userId); + return bookingService.findById(userId, bookingId); + } + + @PostMapping + public BookingDto create(@RequestHeader("X-Sharer-User-Id") Long userId, + @RequestBody BookingNewDto bookingNewDto) { + log.info("Запрос на создание бронирования с Id:{} от пользователя с Id:{}", userId, bookingNewDto.getItemId()); + return bookingService.create(userId, bookingNewDto); + } + + @PatchMapping("/{bookingId}") + public BookingDto approve(@RequestHeader("X-Sharer-User-Id") Long userId, + @PathVariable Long bookingId, + @RequestParam(name = "approved") Boolean isApproved) { + log.info("Запрос на подтверждение или отклонение бронирования с Id:{}", bookingId); + return bookingService.approve(userId, bookingId, isApproved); + } + + @GetMapping("/owner") + public Collection findByOwnerId(@RequestHeader("X-Sharer-User-Id") Long ownerId, + @RequestParam(name = "state", defaultValue = "ALL") + BookingState bookingState) { + log.info("Запрос на получение списка бронирований владельца с id:{}", ownerId); + return bookingService.findByOwnerId(ownerId, bookingState); + } + + @GetMapping + public Collection findByBookerId(@RequestHeader("X-Sharer-User-Id") Long bookerId, + @RequestParam(name = "state", defaultValue = "ALL") + BookingState bookingState) { + log.info("Запрос на получение списка бронирований пользователя с id:{}", bookerId); + return bookingService.findByBookerId(bookerId, bookingState); + } +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/booking/BookingService.java b/src/main/java/ru/practicum/shareit/booking/BookingService.java new file mode 100644 index 0000000..4edab6b --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/BookingService.java @@ -0,0 +1,20 @@ +package ru.practicum.shareit.booking; + +import ru.practicum.shareit.booking.dto.BookingDto; +import ru.practicum.shareit.booking.dto.BookingNewDto; +import ru.practicum.shareit.booking.model.BookingState; + +import java.util.Collection; + +public interface BookingService { + + BookingDto findById(Long userId, Long bookingId); + + BookingDto create(Long userId, BookingNewDto bookingNewDto); + + BookingDto approve(Long userId, Long bookingId, Boolean isApproved); + + Collection findByOwnerId(Long ownerId, BookingState bookingState); + + Collection findByBookerId(Long bookerId, BookingState bookingState); +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/booking/BookingServiceImpl.java b/src/main/java/ru/practicum/shareit/booking/BookingServiceImpl.java new file mode 100644 index 0000000..26c2589 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/BookingServiceImpl.java @@ -0,0 +1,218 @@ +package ru.practicum.shareit.booking; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import ru.practicum.shareit.booking.dao.BookingRepository; +import ru.practicum.shareit.booking.dto.BookingDto; +import ru.practicum.shareit.booking.dto.BookingNewDto; +import ru.practicum.shareit.booking.mapper.BookingMapper; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.model.BookingState; +import ru.practicum.shareit.booking.model.BookingStatus; +import ru.practicum.shareit.booking.utils.BookingValidate; +import ru.practicum.shareit.exception.*; +import ru.practicum.shareit.exception.ValidationException; +import ru.practicum.shareit.item.dao.ItemRepository; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.user.dao.UserRepository; +import ru.practicum.shareit.user.model.User; + +import java.util.Collection; +import java.util.stream.Collectors; + +@Slf4j +@Service +@RequiredArgsConstructor +public class BookingServiceImpl implements BookingService { + + private final BookingRepository bookingRepository; + private final ItemRepository itemRepository; + private final UserRepository userRepository; + + // Получение бронирования по id + @Override + public BookingDto findById(Long userId, Long bookingId) { + if (userId == null || userId <= 0) { + throw new ValidationException("Некорректный id пользователя"); + } + + if (bookingId == null || bookingId <= 0) { + throw new ValidationException("Некорректный id бронирования"); + } + + Booking booking = bookingRepository.findById(bookingId) + .orElseThrow(() -> new NotFoundException("Бронирование не найдено")); + + // Проверяем права доступа + if (!booking.getBooker().getId().equals(userId) && + !booking.getItem().getOwner().getId().equals(userId)) { + throw new ForbiddenException("Доступ к бронированию запрещен"); + } + + return BookingMapper.mapToDto(booking); + } + + // Добавление бронирования + @Override + public BookingDto create(Long userId, BookingNewDto bookingNewDto) { + if (userId == null || userId <= 0) { + throw new ValidationException("Некорректный ID пользователя"); + } + + if (bookingNewDto == null) { + throw new ValidationException("Бронирование не может быть null"); + } + + User booker = userRepository.findById(userId) + .orElseThrow(() -> new NotFoundException("Пользователь не найден")); + Item item = itemRepository.findById(bookingNewDto.getItemId()) + .orElseThrow(() -> new NotFoundException("Вещь с id:" + bookingNewDto.getItemId() + "не найдена")); + + if (item.getOwner().getId().equals(userId)) { + throw new ForbiddenException("Владелец не может бронировать свою вещь"); + } + + // Проверяем доступность вещи + if (!item.getAvailable()) { + throw new ValidationException("Вещь недоступна для бронирования"); + } + + Booking booking = BookingMapper.mapToBooking(bookingNewDto); + booking.setItem(item); + booking.setBooker(booker); + booking.setStatus(BookingStatus.WAITING); + BookingValidate.validateAll(booking); + + Booking createdBooking = bookingRepository.save(booking); + return BookingMapper.mapToDto(createdBooking); + } + + // Подтверждение бронирования + @Override + public BookingDto approve(Long userId, Long bookingId, Boolean isApproved) { + if (userId == null || userId <= 0) { + throw new ValidationException("Некорректный ID пользователя"); + } + + if (bookingId == null || bookingId <= 0) { + throw new ValidationException("Некорректный ID бронирования"); + } + + if (isApproved == null) { + throw new ValidationException("Статус подтверждения не может быть null"); + } + + Booking booking = bookingRepository.findById(bookingId) + .orElseThrow(() -> new NotFoundException("Бронирование не найдено")); + + // Проверка прав доступа + if (!booking.getItem().getOwner().getId().equals(userId)) { + log.warn("Пользователь ID: {} не является владельцем вещи ID: {}", + userId, booking.getItem().getId()); + throw new ForbiddenException("Только владелец вещи может подтверждать бронирование"); + } + + // Проверка текущего статуса + if (!BookingStatus.WAITING.equals(booking.getStatus())) { + log.warn("Попытка изменить статус бронирования Id: {} с текущим статусом: {}", + bookingId, booking.getStatus()); + throw new ValidationException("Бронирование уже обработано"); + } + + // Установка нового статуса + BookingStatus newStatus = isApproved ? BookingStatus.APPROVED : BookingStatus.REJECTED; + booking.setStatus(newStatus); + + Booking updatedBooking = bookingRepository.save(booking); + log.info("Бронирование Id: {} обновлено со статусом: {}", bookingId, newStatus); + + return BookingMapper.mapToDto(updatedBooking); + } + + // Получение списка бронирований владельца + @Override + public Collection findByOwnerId(Long ownerId, BookingState bookingState) { + if (ownerId == null || ownerId <= 0) { + throw new ValidationException("Некорректный ID пользователя"); + } + + if (bookingState == null) { + throw new ValidationException("Статус бронирования не может быть null"); + } + + userRepository.findById(ownerId) + .orElseThrow(() -> new NotFoundException("Пользователь с id:" + ownerId + " не найден")); + + Collection bookings; + switch (bookingState) { + case ALL: + bookings = bookingRepository.findByItemOwnerId(ownerId); + break; + case CURRENT: + bookings = bookingRepository.findByOwnerIdStateCurrent(ownerId); + break; + case PAST: + bookings = bookingRepository.findByOwnerIdStatePast(ownerId); + break; + case FUTURE: + bookings = bookingRepository.findByOwnerIdStateFuture(ownerId); + break; + case WAITING: + bookings = bookingRepository.findByItemOwnerIdAndStatus(ownerId, BookingStatus.WAITING); + break; + case REJECTED: + bookings = bookingRepository.findByItemOwnerIdAndStatus(ownerId, BookingStatus.REJECTED); + break; + default: + throw new ValidationException("Неизвестное состояние бронирования"); + } + + return bookings.stream() + .map(BookingMapper::mapToDto) + .collect(Collectors.toList()); + } + + // Получение списка бронирований пользователя + @Override + public Collection findByBookerId(Long bookerId, BookingState bookingState) { + if (bookerId == null || bookerId <= 0) { + throw new ValidationException("Некорректный ID пользователя"); + } + + if (bookingState == null) { + throw new ValidationException("Статус бронирования не может быть null"); + } + + userRepository.findById(bookerId) + .orElseThrow(() -> new NotFoundException("Пользователь с id:" + bookerId + " не найден")); + + Collection bookings; + switch (bookingState) { + case ALL: + bookings = bookingRepository.findByBookerId(bookerId); + break; + case CURRENT: + bookings = bookingRepository.findByBookerIdStateCurrent(bookerId); + break; + case PAST: + bookings = bookingRepository.findByOwnerIdStatePast(bookerId); + break; + case FUTURE: + bookings = bookingRepository.findByBookerIdStateFuture(bookerId); + break; + case WAITING: + bookings = bookingRepository.findByBookerIdAndStatus(bookerId, BookingStatus.WAITING); + break; + case REJECTED: + bookings = bookingRepository.findByBookerIdAndStatus(bookerId, BookingStatus.REJECTED); + break; + default: + throw new ValidationException("Неизвестное состояние бронирования"); + } + + return bookings.stream() + .map(BookingMapper::mapToDto) + .collect(Collectors.toList()); + } +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/booking/controller/BookingController.java b/src/main/java/ru/practicum/shareit/booking/controller/BookingController.java deleted file mode 100644 index 427a4b6..0000000 --- a/src/main/java/ru/practicum/shareit/booking/controller/BookingController.java +++ /dev/null @@ -1,12 +0,0 @@ -package ru.practicum.shareit.booking.controller; - -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -/** - * TODO Sprint add-bookings. - */ -@RestController -@RequestMapping(path = "/bookings") -public class BookingController { -} diff --git a/src/main/java/ru/practicum/shareit/booking/dao/BookingRepository.java b/src/main/java/ru/practicum/shareit/booking/dao/BookingRepository.java new file mode 100644 index 0000000..6cca226 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/dao/BookingRepository.java @@ -0,0 +1,81 @@ +package ru.practicum.shareit.booking.dao; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.model.BookingStatus; + +import java.util.Collection; + +public interface BookingRepository extends JpaRepository { + + Collection findByBookerId(Long bookerId); + + @Query("SELECT b FROM Booking b " + + "WHERE b.booker.id = :bookerId " + + "AND b.start <= CURRENT_TIMESTAMP " + + "AND b.end >= CURRENT_TIMESTAMP " + + "AND b.status = 'APPROVED'") + Collection findByBookerIdStateCurrent(Long bookerId); + + @Query("SELECT b FROM Booking b " + + "WHERE b.booker.id = :bookerId " + + "AND b.end < CURRENT_TIMESTAMP " + + "AND b.status = 'APPROVED'") + Collection findByBookerIdStatePast(Long bookerId); + + @Query("SELECT b FROM Booking b " + + "WHERE b.booker.id = :bookerId " + + "AND b.start > CURRENT_TIMESTAMP " + + "AND b.status = 'APPROVED'") + Collection findByBookerIdStateFuture(Long bookerId); + + Collection findByBookerIdAndStatus(Long bookerId, BookingStatus status); + + Collection findByItemOwnerId(Long ownerId); + + @Query("SELECT b FROM Booking b " + + "WHERE b.item.owner.id = :ownerId " + + "AND b.start <= CURRENT_TIMESTAMP " + + "AND b.end >= CURRENT_TIMESTAMP " + + "AND b.status = 'APPROVED'") + Collection findByOwnerIdStateCurrent(Long ownerId); + + @Query("SELECT b FROM Booking b " + + "WHERE b.item.owner.id = :ownerId " + + "AND b.end < CURRENT_TIMESTAMP " + + "AND b.status = 'APPROVED'") + Collection findByOwnerIdStatePast(Long ownerId); + + @Query("SELECT b FROM Booking b " + + "WHERE b.item.owner.id = :ownerId " + + "AND b.start > CURRENT_TIMESTAMP " + + "AND b.status = 'APPROVED'") + Collection findByOwnerIdStateFuture(Long ownerId); + + Collection findByItemOwnerIdAndStatus(Long ownerId, BookingStatus status); + + @Query("SELECT b FROM Booking b " + + "WHERE b.item.id = :itemId " + + "AND b.item.owner.id = :ownerId " + + "AND b.start <= CURRENT_TIMESTAMP " + + "AND b.status = 'APPROVED' " + + "ORDER BY b.start DESC") + Collection findByOwnerIdLastBookingsForItem(Long itemId, Long ownerId); + + @Query("SELECT b FROM Booking b " + + "WHERE b.item.id = :itemId " + + "AND b.item.owner.id = :ownerId " + + "AND b.start >= CURRENT_TIMESTAMP " + + "AND b.status = 'APPROVED' " + + "ORDER BY b.start ASC") + Collection findByOwnerIdNextBookingsForItem(Long itemId, Long ownerId); + + @Query("SELECT b FROM Booking b " + + "WHERE b.booker.id = :bookerId " + + "AND b.item.id = :itemId " + + "AND b.end <= CURRENT_TIMESTAMP " + + "AND b.status = 'APPROVED' " + + "ORDER BY b.end DESC") + Collection findByBookerIdLastBookingsForItem(Long bookerId, Long itemId); +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/booking/dto/BookingDto.java b/src/main/java/ru/practicum/shareit/booking/dto/BookingDto.java index 861de9e..bf2e2e1 100644 --- a/src/main/java/ru/practicum/shareit/booking/dto/BookingDto.java +++ b/src/main/java/ru/practicum/shareit/booking/dto/BookingDto.java @@ -1,7 +1,34 @@ package ru.practicum.shareit.booking.dto; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import ru.practicum.shareit.booking.model.BookingStatus; +import ru.practicum.shareit.item.dto.ItemDto; +import ru.practicum.shareit.user.dto.UserDto; + +import java.time.Instant; + /** * TODO Sprint add-bookings. */ +@Getter +@Setter +@Builder(toBuilder = true) public class BookingDto { -} + + private Long id; + + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss", shape = JsonFormat.Shape.STRING, timezone = "UTC") + private Instant start; + + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss", shape = JsonFormat.Shape.STRING, timezone = "UTC") + private Instant end; + + private ItemDto item; + + private UserDto booker; + + private BookingStatus status; +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/booking/dto/BookingNewDto.java b/src/main/java/ru/practicum/shareit/booking/dto/BookingNewDto.java new file mode 100644 index 0000000..3d70ac2 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/dto/BookingNewDto.java @@ -0,0 +1,22 @@ +package ru.practicum.shareit.booking.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.*; + +import java.time.Instant; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@Builder(toBuilder = true) +public class BookingNewDto { + + private Long itemId; + + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss", shape = JsonFormat.Shape.STRING, timezone = "UTC") + private Instant start; + + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss", shape = JsonFormat.Shape.STRING, timezone = "UTC") + private Instant end; +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/booking/mapper/BookingMapper.java b/src/main/java/ru/practicum/shareit/booking/mapper/BookingMapper.java new file mode 100644 index 0000000..5834af5 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/mapper/BookingMapper.java @@ -0,0 +1,32 @@ +package ru.practicum.shareit.booking.mapper; + +import ru.practicum.shareit.booking.dto.BookingDto; +import ru.practicum.shareit.booking.dto.BookingNewDto; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.model.BookingStatus; +import ru.practicum.shareit.item.mapper.ItemMapper; +import ru.practicum.shareit.user.mapper.UserMapper; + +public class BookingMapper { + + // Преобразование Booking в DTO + public static BookingDto mapToDto(Booking booking) { + return BookingDto.builder() + .id(booking.getId()) + .start(booking.getStart()) + .end(booking.getEnd()) + .item(ItemMapper.mapToDto(booking.getItem())) + .booker(UserMapper.mapToDto(booking.getBooker())) + .status(booking.getStatus()) + .build(); + } + + // Преобразование BookingNewDto в Booking + public static Booking mapToBooking(BookingNewDto bookingNewDto) { + return Booking.builder() + .start(bookingNewDto.getStart()) + .end(bookingNewDto.getEnd()) + .status(BookingStatus.WAITING) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/booking/model/Booking.java b/src/main/java/ru/practicum/shareit/booking/model/Booking.java index 71f189c..e6d2dea 100644 --- a/src/main/java/ru/practicum/shareit/booking/model/Booking.java +++ b/src/main/java/ru/practicum/shareit/booking/model/Booking.java @@ -1,7 +1,56 @@ package ru.practicum.shareit.booking.model; +import jakarta.persistence.*; +import lombok.*; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.user.model.User; + +import java.time.Instant; + /** * TODO Sprint add-bookings. */ +@Entity +@Table(name = "bookings", schema = "public") +@Getter +@Setter +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Builder(toBuilder = true) public class Booking { -} + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "start_date", nullable = false) + private Instant start; + + @Column(name = "end_date", nullable = false) + private Instant end; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "item_id", nullable = false) + private Item item; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "booker_id", nullable = false) + private User booker; + + @Enumerated(EnumType.STRING) + @Column(name = "status", nullable = false) + private BookingStatus status; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Booking)) return false; + return id != null && id.equals(((Booking) o).getId()); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/booking/model/BookingState.java b/src/main/java/ru/practicum/shareit/booking/model/BookingState.java new file mode 100644 index 0000000..3e25892 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/model/BookingState.java @@ -0,0 +1,5 @@ +package ru.practicum.shareit.booking.model; + +public enum BookingState { + ALL, CURRENT, PAST, FUTURE, WAITING, REJECTED +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/booking/model/BookingStatus.java b/src/main/java/ru/practicum/shareit/booking/model/BookingStatus.java new file mode 100644 index 0000000..14f2621 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/model/BookingStatus.java @@ -0,0 +1,5 @@ +package ru.practicum.shareit.booking.model; + +public enum BookingStatus { + WAITING, APPROVED, REJECTED, CANCELED; +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/booking/utils/BookingValidate.java b/src/main/java/ru/practicum/shareit/booking/utils/BookingValidate.java new file mode 100644 index 0000000..6b77d98 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/utils/BookingValidate.java @@ -0,0 +1,65 @@ +package ru.practicum.shareit.booking.utils; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.exception.ValidationException; + +import java.time.Instant; + +/** + * Утилитарный класс для валидации данных сущности Booking + */ +@Slf4j +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class BookingValidate { + + // Валидация даты начала бронирования + public static void validateForStartBooking(Booking booking) { + validateNotNull(booking); + if (booking.getStart().isBefore(Instant.now())) { + log.warn("Задана некорректная дата начала бронирования"); + throw new ValidationException("Дата начала бронирования не может быть в прошлом"); + } + } + + // Валидация даты окончания бронирования + public static void validateForEndBooking(Booking booking) { + validateNotNull(booking); + if (booking.getEnd().isBefore(Instant.now())) { + log.warn("Задана некорректная дата окончания бронирования"); + throw new ValidationException("Дата окончания бронирования не может быть в прошлом"); + } + } + + // Валидация корректности временного интервала между началом и окончанием бронирования + public static void validateTimeInterval(Booking booking) { + validateNotNull(booking); + if (booking.getEnd().isBefore(booking.getStart()) || booking.getEnd().equals(booking.getStart())) { + log.warn("Задан некорректный интервал между началом и окончанием бронирования"); + throw new ValidationException("Дата окончания бронирования должна быть позже даты начала"); + } + } + + // Комплексная валидация для Booking + public static void validateAll(Booking booking) { + validateNotNull(booking); + validateForStartBooking(booking); + validateForEndBooking(booking); + validateTimeInterval(booking); + } + + // Вспомогательный метод проверки бронирования на null + private static void validateNotNull(Booking booking) { + if (booking == null) { + log.error("Бронирование не может быть null"); + throw new ValidationException("Бронирование не может быть null"); + } + + if (booking.getStart() == null || booking.getEnd() == null) { + log.error("Некорректная дата начала или дата окончания бронирования"); + throw new ValidationException("Дата начала или дата окончания не может быть null"); + } + } +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/exception/ErrorResponse.java b/src/main/java/ru/practicum/shareit/exception/ErrorResponse.java index 736d166..0102bec 100644 --- a/src/main/java/ru/practicum/shareit/exception/ErrorResponse.java +++ b/src/main/java/ru/practicum/shareit/exception/ErrorResponse.java @@ -1,9 +1,9 @@ package ru.practicum.shareit.exception; -import lombok.Getter; +import lombok.Data; import lombok.RequiredArgsConstructor; -@Getter +@Data @RequiredArgsConstructor public class ErrorResponse { private final String error; diff --git a/src/main/java/ru/practicum/shareit/item/ItemController.java b/src/main/java/ru/practicum/shareit/item/ItemController.java new file mode 100644 index 0000000..73c9b62 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/ItemController.java @@ -0,0 +1,76 @@ +package ru.practicum.shareit.item; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; +import ru.practicum.shareit.item.comment.dto.CommentDto; +import ru.practicum.shareit.item.comment.dto.CommentNewDto; +import ru.practicum.shareit.item.dto.ItemDto; +import ru.practicum.shareit.item.dto.ItemFullDto; +import ru.practicum.shareit.item.dto.ItemNewDto; + +import java.util.Collection; + +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/items") +public class ItemController { + + private final ItemService itemService; + + @GetMapping + public Collection findAllByUserId(@RequestHeader("X-Sharer-User-Id") Long userId) { + log.info("Запрос на получение всех вещей пользователя c Id:{}", userId); + return itemService.findAllByUserId(userId); + } + + @GetMapping("/{itemId}") + public ItemFullDto findById(@RequestHeader("X-Sharer-User-Id") Long userId, + @PathVariable Long itemId) { + log.info("Запрос на получение вещи с Id:{}", itemId); + return itemService.findById(userId, itemId); + } + + @GetMapping("/search") + public Collection searchByText(@RequestParam("text") String searchText) { + log.info("Запрос на поиск вещи в названии или в описании"); + return itemService.searchByText(searchText); + } + + @PostMapping + public ItemDto create(@RequestHeader("X-Sharer-User-Id") Long userId, + @RequestBody ItemNewDto newItem) { + log.info("Запрос на добавление новой вещи пользователем с Id:{}", userId); + return itemService.create(userId, newItem); + } + + @PatchMapping("/{itemId}") + public ItemDto update(@RequestHeader("X-Sharer-User-Id") Long ownerId, + @PathVariable Long itemId, + @RequestBody ItemNewDto updatedItem) { + log.info("Запрос на обновление вещи с Id:{} пользователем с Id:{}", itemId, ownerId); + return itemService.update(ownerId, itemId, updatedItem); + } + + @DeleteMapping("/{itemId}") + public void delete(@RequestHeader("X-Sharer-User-Id") Long ownerId, + @PathVariable Long itemId) { + log.info("Запрос на удаление вещи с Id:{} пользователем с Id:{}", itemId, ownerId); + itemService.delete(ownerId, itemId); + } + + @PostMapping("/{itemId}/comment") + public CommentDto addComment(@RequestHeader("X-Sharer-User-Id") Long userId, + @PathVariable Long itemId, + @RequestBody CommentNewDto comment) { + log.info("Запрос на добавление комментария вещи с Id:{} пользователем с Id:{}", itemId, userId); + return itemService.addComment(userId, itemId, comment); + } + + @GetMapping("/{itemId}/comment") + public Collection findAllCommentsByItemId(@PathVariable Long itemId) { + log.info("Запрос на получение всех комментариев к вещи с id:{}", itemId); + return itemService.findAllCommentsByItemId(itemId); + } +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/item/ItemService.java b/src/main/java/ru/practicum/shareit/item/ItemService.java new file mode 100644 index 0000000..eb9d1bd --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/ItemService.java @@ -0,0 +1,27 @@ +package ru.practicum.shareit.item; + +import ru.practicum.shareit.item.comment.dto.CommentDto; +import ru.practicum.shareit.item.comment.dto.CommentNewDto; +import ru.practicum.shareit.item.dto.ItemDto; +import ru.practicum.shareit.item.dto.ItemFullDto; +import ru.practicum.shareit.item.dto.ItemNewDto; + +import java.util.Collection; + +public interface ItemService { + Collection findAllByUserId(Long userId); + + ItemFullDto findById(Long userId, Long itemId); + + Collection searchByText(String searchText); + + ItemDto create(Long ownerId, ItemNewDto newItem); + + ItemDto update(Long ownerId, Long itemId, ItemNewDto updatedItem); + + void delete(Long ownerID, Long itemId); + + CommentDto addComment(Long userId, Long itemId, CommentNewDto comment); + + Collection findAllCommentsByItemId(Long itemId); +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java b/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java new file mode 100644 index 0000000..a6cdb97 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java @@ -0,0 +1,198 @@ +package ru.practicum.shareit.item; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import ru.practicum.shareit.booking.dao.BookingRepository; +import ru.practicum.shareit.booking.dto.BookingDto; +import ru.practicum.shareit.booking.mapper.BookingMapper; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.exception.*; +import ru.practicum.shareit.item.comment.dao.CommentRepository; +import ru.practicum.shareit.item.comment.dto.CommentDto; +import ru.practicum.shareit.item.comment.dto.CommentNewDto; +import ru.practicum.shareit.item.comment.mapper.CommentMapper; +import ru.practicum.shareit.item.comment.model.Comment; +import ru.practicum.shareit.item.dao.ItemRepository; +import ru.practicum.shareit.item.dto.ItemDto; +import ru.practicum.shareit.item.dto.ItemFullDto; +import ru.practicum.shareit.item.dto.ItemNewDto; +import ru.practicum.shareit.item.mapper.ItemMapper; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.item.utils.ItemValidate; +import ru.practicum.shareit.user.dao.UserRepository; +import ru.practicum.shareit.user.model.User; + +import java.time.Instant; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ItemServiceImpl implements ItemService { + + private final ItemRepository itemRepository; + private final UserRepository userRepository; + private final BookingRepository bookingRepository; + private final CommentRepository commentRepository; + + // Получение всех вещей пользователя + @Override + public Collection findAllByUserId(Long userId) { + return itemRepository.findAllByOwnerId(userId) + .stream() + .map(item -> { + BookingDto lastBooking = findLastBooking(item.getId(), userId); + BookingDto nextBooking = findNextBooking(item.getId(), userId); + List comments = findCommentsForItem(item.getId()); + return ItemMapper.mapToFullDto(item, lastBooking, nextBooking, comments); + }) + .collect(Collectors.toList()); + } + + // Получение вещи по id + @Override + public ItemFullDto findById(Long userId, Long itemId) { + Item item = itemRepository.findById(itemId) + .orElseThrow(() -> new NotFoundException("Вещь не найдена")); + + BookingDto lastBooking = findLastBooking(itemId, userId); + BookingDto nextBooking = findNextBooking(itemId, userId); + List comments = findCommentsForItem(itemId); + + return ItemMapper.mapToFullDto(item, lastBooking, nextBooking, comments); + } + + // Поиск вещи по тексту в названии или описании + @Override + public Collection searchByText(String searchText) { + if (searchText == null || searchText.trim().isEmpty()) { + return Collections.emptyList(); + } + + return itemRepository.searchByText(searchText.toLowerCase()) + .stream() + .map(ItemMapper::mapToDto) + .collect(Collectors.toList()); + } + + // Добавление вещи + @Override + public ItemDto create(Long ownerId, ItemNewDto itemNewDto) { + ItemValidate.validateForCreate(itemNewDto); + User owner = userRepository.findById(ownerId) + .orElseThrow(() -> new NotFoundException("Пользователь с ID" + ownerId + " не найден")); + Item item = ItemMapper.mapToItem(itemNewDto, owner); + Item createdItem = itemRepository.save(item); + return ItemMapper.mapToDto(createdItem); + } + + // Обновление вещи + @Override + public ItemDto update(Long ownerId, Long itemId, ItemNewDto itemNewDto) { + ItemValidate.validateForUpdate(itemNewDto); + + Item existingItem = itemRepository.findById(itemId) + .orElseThrow(() -> new NotFoundException("Вещь с ID " + itemId + "не найдена")); + + if (!existingItem.getOwner().getId().equals(ownerId)) { + log.warn("Попытка изменения чужой вещи"); + throw new ForbiddenException("Пользователь не является владельцем вещи"); + } + + if (itemNewDto.getName() != null) { + existingItem.setName(itemNewDto.getName()); + } + if (itemNewDto.getDescription() != null) { + existingItem.setDescription(itemNewDto.getDescription()); + } + if (itemNewDto.getAvailable() != null) { + existingItem.setAvailable(itemNewDto.getAvailable()); + } + + Item updatedItem = itemRepository.save(existingItem); + return ItemMapper.mapToDto(updatedItem); + } + + // Удаление вещи + @Override + public void delete(Long ownerId, Long itemId) { + Item item = itemRepository.findById(itemId) + .orElseThrow(() -> new NotFoundException("Вещь с ID " + itemId + "не найдена")); + + if (!item.getOwner().getId().equals(ownerId)) { + log.warn("Попытка удаления чужой вещи. Пользователь ID: {}, Владелец вещи ID: {}", + ownerId, item.getOwner().getId()); + throw new ForbiddenException("Недостаточно прав для удаления вещи"); + } + + itemRepository.deleteById(itemId); + } + + // Добавление комментариев вещи + @Override + public CommentDto addComment(Long userId, Long itemId, CommentNewDto commentNewDto) { + User author = userRepository.findById(userId) + .orElseThrow(() -> new NotFoundException("Пользователь с ID " + userId + " не найден")); + + Item item = itemRepository.findById(itemId) + .orElseThrow(() -> new NotFoundException("Вещь с ID " + itemId + "не найдена")); + + Collection bookings = bookingRepository.findByBookerIdLastBookingsForItem(userId, itemId); + if (bookings.isEmpty()) { + log.error("Пользователь с id:{} не бронировал вещь с id:{}", userId, itemId); + throw new ValidationException("Нельзя оставить комментарий, если пользователь не бронировал вещь"); + } + + Comment comment = Comment.builder() + .text(commentNewDto.getText()) + .item(item) + .author(author) + .created(Instant.now()) + .build(); + Comment savedComment = commentRepository.save(comment); + return CommentMapper.mapToDto(savedComment); + } + + // Получение всех комментариев вещи + @Override + public Collection findAllCommentsByItemId(Long itemId) { + if (!itemRepository.existsById(itemId)) { + throw new NotFoundException("Вещь с ID " + itemId + " не найдена"); + } + + return commentRepository.findByItemId(itemId) + .stream() + .map(CommentMapper::mapToDto) + .collect(Collectors.toList()); + } + + // Вспомогательный метод для поиска последнего бронирования для вещи + private BookingDto findLastBooking(Long itemId, Long userId) { + Collection lastBookings = bookingRepository.findByOwnerIdLastBookingsForItem(itemId, userId); + return lastBookings.stream() + .findFirst() + .map(BookingMapper::mapToDto) + .orElse(null); + } + + // Вспомогательный метод для поиска следующего бронирования для вещи + private BookingDto findNextBooking(Long itemId, Long userId) { + Collection nextBookings = bookingRepository.findByOwnerIdNextBookingsForItem(itemId, userId); + return nextBookings.stream() + .findFirst() + .map(BookingMapper::mapToDto) + .orElse(null); + } + + // Вспомогательный метод для получения комментариев для вещи + private List findCommentsForItem(Long itemId) { + return commentRepository.findByItemId(itemId) + .stream() + .map(CommentMapper::mapToDto) + .collect(Collectors.toList()); + } +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/item/comment/dao/CommentRepository.java b/src/main/java/ru/practicum/shareit/item/comment/dao/CommentRepository.java new file mode 100644 index 0000000..41ebc7f --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/comment/dao/CommentRepository.java @@ -0,0 +1,11 @@ +package ru.practicum.shareit.item.comment.dao; + +import org.springframework.data.jpa.repository.JpaRepository; +import ru.practicum.shareit.item.comment.model.Comment; + +import java.util.Collection; + +public interface CommentRepository extends JpaRepository { + + Collection findByItemId(Long itemId); +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/item/comment/dto/CommentDto.java b/src/main/java/ru/practicum/shareit/item/comment/dto/CommentDto.java new file mode 100644 index 0000000..d1623c2 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/comment/dto/CommentDto.java @@ -0,0 +1,23 @@ +package ru.practicum.shareit.item.comment.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +import java.time.Instant; + +@Getter +@Setter +@Builder(toBuilder = true) +public class CommentDto { + + private Long id; + + private String text; + + private String authorName; + + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss", shape = JsonFormat.Shape.STRING, timezone = "UTC") + private Instant created; +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/item/comment/dto/CommentNewDto.java b/src/main/java/ru/practicum/shareit/item/comment/dto/CommentNewDto.java new file mode 100644 index 0000000..9ae58cb --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/comment/dto/CommentNewDto.java @@ -0,0 +1,13 @@ +package ru.practicum.shareit.item.comment.dto; + +import lombok.*; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@Builder(toBuilder = true) +public class CommentNewDto { + + private String text; +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/item/comment/mapper/CommentMapper.java b/src/main/java/ru/practicum/shareit/item/comment/mapper/CommentMapper.java new file mode 100644 index 0000000..b2ad932 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/comment/mapper/CommentMapper.java @@ -0,0 +1,32 @@ +package ru.practicum.shareit.item.comment.mapper; + +import ru.practicum.shareit.item.comment.dto.CommentDto; +import ru.practicum.shareit.item.comment.dto.CommentNewDto; +import ru.practicum.shareit.item.comment.model.Comment; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.user.model.User; + +import java.time.Instant; + +public class CommentMapper { + + // Преобразование Comment в Dto + public static CommentDto mapToDto(Comment comment) { + return CommentDto.builder() + .id(comment.getId()) + .text(comment.getText()) + .authorName(comment.getAuthor().getName()) + .created(comment.getCreated()) + .build(); + } + + // Преобразование CommentNewDto в Comment + public static Comment mapToComment(CommentNewDto commentNewDto, Item item, User author) { + return Comment.builder() + .text(commentNewDto.getText()) + .item(item) + .author(author) + .created(Instant.now()) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/item/comment/model/Comment.java b/src/main/java/ru/practicum/shareit/item/comment/model/Comment.java new file mode 100644 index 0000000..0e6e6af --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/comment/model/Comment.java @@ -0,0 +1,55 @@ +package ru.practicum.shareit.item.comment.model; + +import jakarta.persistence.*; +import lombok.*; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.user.model.User; + +import java.time.Instant; + +@Entity +@Table(name = "comments", schema = "public") +@Getter +@Setter +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Builder(toBuilder = true) +public class Comment { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "text", nullable = false) + private String text; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "item_id", nullable = false) + private Item item; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "author_id", nullable = false) + private User author; + + @Column(name = "created", nullable = false) + private Instant created; + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (!(o instanceof Comment)) { + return false; + } + + return id != null && id.equals(((Comment) o).getId()); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/item/controller/ItemController.java b/src/main/java/ru/practicum/shareit/item/controller/ItemController.java deleted file mode 100644 index 3fb7b92..0000000 --- a/src/main/java/ru/practicum/shareit/item/controller/ItemController.java +++ /dev/null @@ -1,62 +0,0 @@ -package ru.practicum.shareit.item.controller; - -import lombok.NonNull; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.web.bind.annotation.*; -import ru.practicum.shareit.item.dto.ItemDto; -import ru.practicum.shareit.item.service.ItemService; - -import java.util.Collection; - -/** - * TODO Sprint add-controllers. - */ -@Slf4j -@RestController -@RequiredArgsConstructor -@RequestMapping("/items") -public class ItemController { - - private final ItemService itemService; - - @GetMapping - public Collection findAllByUserId(@NonNull @RequestHeader("X-Sharer-User-Id") Long userId) { - log.info("Запрос на получение всех вещей пользователя c Id:{}", userId); - return itemService.findAllByUserId(userId); - } - - @GetMapping("/{itemId}") - public ItemDto findById(@NonNull @PathVariable Long itemId) { - log.info("Запрос на получение вещи с Id:{}", itemId); - return itemService.findById(itemId); - } - - @GetMapping("/search") - public Collection searchByText(@NonNull @RequestParam("text") String searchText) { - log.info("Запрос на поиск вещи в названии или в описании"); - return itemService.searchByText(searchText); - } - - @PostMapping - public ItemDto create(@NonNull @RequestHeader("X-Sharer-User-Id") Long userId, - @NonNull @RequestBody ItemDto newItem) { - log.info("Запрос на добавление новой вещи пользователем с Id:{}", userId); - return itemService.create(userId, newItem); - } - - @PatchMapping("/{itemId}") - public ItemDto update(@NonNull @RequestHeader("X-Sharer-User-Id") Long ownerId, - @NonNull @PathVariable Long itemId, - @NonNull @RequestBody ItemDto updatedItem) { - log.info("Запрос на обновление вещи с Id:{} пользователем с Id:{}", itemId, ownerId); - return itemService.update(ownerId, itemId, updatedItem); - } - - @DeleteMapping("/{itemId}") - public void delete(@NonNull @RequestHeader("X-Sharer-User-Id") Long ownerId, - @NonNull @PathVariable Long itemId) { - log.info("Запрос на удаление вещи с Id:{} пользователем с Id:{}", itemId, ownerId); - itemService.delete(ownerId, itemId); - } -} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/item/dao/ItemRepository.java b/src/main/java/ru/practicum/shareit/item/dao/ItemRepository.java index fb3fb1e..e401e05 100644 --- a/src/main/java/ru/practicum/shareit/item/dao/ItemRepository.java +++ b/src/main/java/ru/practicum/shareit/item/dao/ItemRepository.java @@ -1,20 +1,18 @@ package ru.practicum.shareit.item.dao; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import ru.practicum.shareit.item.model.Item; import java.util.Collection; -import java.util.Optional; -public interface ItemRepository { - Collection findAllByUserId(Long userId); +public interface ItemRepository extends JpaRepository { - Optional findById(Long itemId); + Collection findAllByOwnerId(Long ownerId); + @Query("SELECT i FROM Item i " + + "WHERE (LOWER(i.name) LIKE LOWER(CONCAT('%', :searchText, '%')) " + + "OR LOWER(i.description) LIKE LOWER(CONCAT('%', :searchText, '%'))) " + + "AND i.available = true") Collection searchByText(String searchText); - - Item create(Item item, Long userId); - - Item update(Item item); - - void delete(Long itemId); } \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/item/dao/ItemRepositoryImpl.java b/src/main/java/ru/practicum/shareit/item/dao/ItemRepositoryImpl.java deleted file mode 100644 index ef17a63..0000000 --- a/src/main/java/ru/practicum/shareit/item/dao/ItemRepositoryImpl.java +++ /dev/null @@ -1,61 +0,0 @@ -package ru.practicum.shareit.item.dao; - -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Repository; -import ru.practicum.shareit.item.model.Item; - -import java.util.*; - -@Slf4j -@Repository -public class ItemRepositoryImpl implements ItemRepository { - - private final Map items = new HashMap<>(); - private final Map> userItemIndex = new HashMap<>(); - private long idCounter = 0L; - - @Override - public Collection findAllByUserId(Long userId) { - return items.values() - .stream() - .filter(item -> item.getOwnerId().equals(userId)) - .toList(); - } - - @Override - public Optional findById(Long itemId) { - return Optional.ofNullable(items.get(itemId)); - } - - @Override - public Collection searchByText(String searchText) { - String text = searchText.toLowerCase(); - return items.values() - .stream() - .filter(item -> (item.getName().toLowerCase().contains(text)) - || (item.getDescription().toLowerCase().contains(text)) - && (item.getAvailable().equals(true))) - .toList(); - } - - @Override - public Item create(Item item, Long userId) { - item.setItemId(++idCounter); - item.setOwnerId(userId); - items.put(item.getItemId(), item); - final List itemsByOwner = userItemIndex.computeIfAbsent(item.getOwnerId(), k -> new ArrayList<>()); - itemsByOwner.add(item); - return item; - } - - @Override - public Item update(Item item) { - items.put(item.getItemId(), item); - return item; - } - - @Override - public void delete(Long itemId) { - items.remove(itemId); - } -} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java b/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java index 84900c1..bd15707 100644 --- a/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java +++ b/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java @@ -1,9 +1,9 @@ package ru.practicum.shareit.item.dto; -import lombok.Builder; -import lombok.Data; +import lombok.*; -@Data +@Getter +@Setter @Builder(toBuilder = true) public class ItemDto { private Long id; diff --git a/src/main/java/ru/practicum/shareit/item/dto/ItemFullDto.java b/src/main/java/ru/practicum/shareit/item/dto/ItemFullDto.java new file mode 100644 index 0000000..832f94a --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/dto/ItemFullDto.java @@ -0,0 +1,24 @@ +package ru.practicum.shareit.item.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import ru.practicum.shareit.booking.dto.BookingDto; +import ru.practicum.shareit.item.comment.dto.CommentDto; + +import java.util.List; + +@Getter +@Setter +@Builder(toBuilder = true) +public class ItemFullDto { + + private Long id; + private String name; + private String description; + private Boolean available; + private Long ownerId; + private BookingDto lastBooking; + private BookingDto nextBooking; + private List comments; +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/item/dto/ItemNewDto.java b/src/main/java/ru/practicum/shareit/item/dto/ItemNewDto.java new file mode 100644 index 0000000..25c5e82 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/dto/ItemNewDto.java @@ -0,0 +1,15 @@ +package ru.practicum.shareit.item.dto; + +import lombok.*; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@Builder(toBuilder = true) +public class ItemNewDto { + + private String name; + private String description; + private Boolean available; +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/item/mapper/ItemMapper.java b/src/main/java/ru/practicum/shareit/item/mapper/ItemMapper.java index dfdf6b9..38fd288 100644 --- a/src/main/java/ru/practicum/shareit/item/mapper/ItemMapper.java +++ b/src/main/java/ru/practicum/shareit/item/mapper/ItemMapper.java @@ -1,29 +1,50 @@ package ru.practicum.shareit.item.mapper; +import ru.practicum.shareit.booking.dto.BookingDto; +import ru.practicum.shareit.item.comment.dto.CommentDto; import ru.practicum.shareit.item.dto.ItemDto; +import ru.practicum.shareit.item.dto.ItemFullDto; +import ru.practicum.shareit.item.dto.ItemNewDto; import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.user.model.User; + +import java.util.List; public class ItemMapper { // Преобразование Item в Dto public static ItemDto mapToDto(Item item) { return ItemDto.builder() - .id(item.getItemId()) + .id(item.getId()) .name(item.getName()) .description(item.getDescription()) .available(item.getAvailable()) - .ownerId(item.getOwnerId()) + .ownerId(item.getOwner().getId()) .build(); } - // Преобразование ItemDto в Item - public static Item mapToItem(ItemDto itemDto) { + // Преобразование ItemNewDto в Item + public static Item mapToItem(ItemNewDto itemNewDto, User owner) { return Item.builder() - .itemId(itemDto.getId()) - .name(itemDto.getName()) - .description(itemDto.getDescription()) - .available(itemDto.getAvailable()) - .ownerId(itemDto.getOwnerId()) + .name(itemNewDto.getName()) + .description(itemNewDto.getDescription()) + .available(itemNewDto.getAvailable()) + .owner(owner) + .build(); + } + + // Преобразование Item в ItemFullDto + public static ItemFullDto mapToFullDto(Item item, BookingDto lastBooking, + BookingDto nextBooking, List comments) { + return ItemFullDto.builder() + .id(item.getId()) + .name(item.getName()) + .description(item.getDescription()) + .available(item.getAvailable()) + .ownerId(item.getOwner().getId()) + .lastBooking(lastBooking) + .nextBooking(nextBooking) + .comments(comments) .build(); } } \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/item/model/Item.java b/src/main/java/ru/practicum/shareit/item/model/Item.java index 640eee5..8f96f2a 100644 --- a/src/main/java/ru/practicum/shareit/item/model/Item.java +++ b/src/main/java/ru/practicum/shareit/item/model/Item.java @@ -1,17 +1,52 @@ package ru.practicum.shareit.item.model; -import lombok.Builder; -import lombok.Data; +import jakarta.persistence.*; +import lombok.*; +import ru.practicum.shareit.user.model.User; -/** - * TODO Sprint add-controllers. - */ -@Data + +@Getter +@Setter +@ToString +@Entity +@Table(name = "items") +@NoArgsConstructor +@AllArgsConstructor @Builder(toBuilder = true) public class Item { - private Long itemId; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "name", nullable = false) private String name; + + @Column(name = "description") private String description; + + @Column(name = "is_available", nullable = false) private Boolean available; - private Long ownerId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "owner_id", nullable = false) + private User owner; + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (!(o instanceof Item)) { + return false; + } + + return id != null && id.equals(((Item) o).getId()); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } } \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/item/service/ItemService.java b/src/main/java/ru/practicum/shareit/item/service/ItemService.java deleted file mode 100644 index cac40a9..0000000 --- a/src/main/java/ru/practicum/shareit/item/service/ItemService.java +++ /dev/null @@ -1,19 +0,0 @@ -package ru.practicum.shareit.item.service; - -import ru.practicum.shareit.item.dto.ItemDto; - -import java.util.Collection; - -public interface ItemService { - Collection findAllByUserId(Long userId); - - ItemDto findById(Long itemId); - - Collection searchByText(String searchText); - - ItemDto create(Long ownerId, ItemDto newItem); - - ItemDto update(Long ownerId, Long itemId, ItemDto updatedItem); - - void delete(Long ownerID, Long itemId); -} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java b/src/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java deleted file mode 100644 index 146ce24..0000000 --- a/src/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java +++ /dev/null @@ -1,107 +0,0 @@ -package ru.practicum.shareit.item.service; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import ru.practicum.shareit.exception.*; -import ru.practicum.shareit.item.dao.ItemRepository; -import ru.practicum.shareit.item.dto.ItemDto; -import ru.practicum.shareit.item.mapper.ItemMapper; -import ru.practicum.shareit.item.model.Item; -import ru.practicum.shareit.item.utils.ItemValidate; -import ru.practicum.shareit.user.dto.UserDto; -import ru.practicum.shareit.user.service.UserService; - -import java.util.Collection; -import java.util.Collections; -import java.util.stream.Collectors; - -@Slf4j -@Service -@RequiredArgsConstructor -public class ItemServiceImpl implements ItemService { - - private final ItemRepository itemRepository; - private final UserService userService; - - // Получение всех вещей пользователя - @Override - public Collection findAllByUserId(Long userId) { - return itemRepository.findAllByUserId(userId) - .stream() - .map(ItemMapper::mapToDto) - .toList(); - } - - // Получение вещи по id - @Override - public ItemDto findById(Long itemId) { - return itemRepository.findById(itemId) - .map(ItemMapper::mapToDto) - .orElseThrow(() -> new NotFoundException("Вещь не найдена")); - } - - // Поиск вещи по тексту в названии или описании - @Override - public Collection searchByText(String searchText) { - if (searchText == null || searchText.trim().isEmpty()) { - return Collections.emptyList(); - } - - return itemRepository.searchByText(searchText.toLowerCase()) - .stream() - .filter(item -> item.getAvailable() != null && item.getAvailable()) - .map(ItemMapper::mapToDto) - .collect(Collectors.toList()); - } - - // Добавление вещи - @Override - public ItemDto create(Long ownerId, ItemDto itemDto) { - UserDto user = userService.findById(ownerId); - - if (user == null) { - throw new NotFoundException("Пользователь с ID" + ownerId + " не найден"); - } - - Item item = ItemMapper.mapToItem(itemDto); - item.setOwnerId(ownerId); - ItemValidate.validateForCreate(item); - - Item createdItem = itemRepository.create(item, ownerId); - return ItemMapper.mapToDto(createdItem); - } - - // Обновление вещи - @Override - public ItemDto update(Long ownerId, Long itemId, ItemDto itemDto) { - userService.findById(ownerId); - - Item existingItem = itemRepository.findById(itemId) - .orElseThrow(() -> new NotFoundException("Вещь с ID " + itemId + "не найдена")); - - if (!existingItem.getOwnerId().equals(ownerId)) { - throw new ForbiddenException("Пользователь не является владельцем вещи"); - } - - if (itemDto.getName() != null) { - existingItem.setName(itemDto.getName()); - } - if (itemDto.getDescription() != null) { - existingItem.setDescription(itemDto.getDescription()); - } - if (itemDto.getAvailable() != null) { - existingItem.setAvailable(itemDto.getAvailable()); - } - - ItemValidate.validateForUpdate(existingItem); - Item updatedItem = itemRepository.update(existingItem); - return ItemMapper.mapToDto(updatedItem); - } - - // Удаление вещи - @Override - public void delete(Long ownerId, Long itemId) { - itemRepository.delete(itemId); - } -} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/item/utils/ItemValidate.java b/src/main/java/ru/practicum/shareit/item/utils/ItemValidate.java index 5b8d629..8412352 100644 --- a/src/main/java/ru/practicum/shareit/item/utils/ItemValidate.java +++ b/src/main/java/ru/practicum/shareit/item/utils/ItemValidate.java @@ -4,7 +4,7 @@ import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; import ru.practicum.shareit.exception.ValidationException; -import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.item.dto.ItemNewDto; /** * Утилитарный класс для валидации сущности Item @@ -15,34 +15,33 @@ public class ItemValidate { // Валидация при создании вещи - public static void validateForCreate(Item item) { - validateNotNull(item); - validateName(item.getName()); - validateDescription(item.getDescription()); - validateAvailability(item.getAvailable()); - validateOwner(item.getOwnerId()); + public static void validateForCreate(ItemNewDto itemNewDto) { + validateNotNull(itemNewDto); + validateName(itemNewDto.getName()); + validateDescription(itemNewDto.getDescription()); + validateAvailability(itemNewDto.getAvailable()); } // Валидация при изменении вещи - public static void validateForUpdate(Item item) { - validateNotNull(item); + public static void validateForUpdate(ItemNewDto itemNewDto) { + validateNotNull(itemNewDto); - if (item.getName() != null) { - validateName(item.getName()); + if (itemNewDto.getName() != null) { + validateName(itemNewDto.getName()); } - if (item.getDescription() != null) { - validateDescription(item.getDescription()); + if (itemNewDto.getDescription() != null) { + validateDescription(itemNewDto.getDescription()); } - if (item.getAvailable() != null) { - validateAvailability(item.getAvailable()); + if (itemNewDto.getAvailable() != null) { + validateAvailability(itemNewDto.getAvailable()); } } // Вспомогательные методы для валидации полей Item - private static void validateNotNull(Item item) { - if (item == null) { + private static void validateNotNull(ItemNewDto itemNewDto) { + if (itemNewDto == null) { log.error("Попытка добавить или изменить null, а не вещь"); throw new ValidationException("Вещь не может быть null"); } @@ -68,11 +67,4 @@ private static void validateAvailability(Boolean available) { throw new ValidationException("Статус доступности вещи должен быть указан"); } } - - private static void validateOwner(Long ownerId) { - if (ownerId == null || ownerId <= 0) { - log.error("Попытка добавить вещь с некорректным Id владельца"); - throw new ValidationException("Некорректный Id владельца"); - } - } } \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/request/model/ItemRequest.java b/src/main/java/ru/practicum/shareit/request/model/ItemRequest.java index 0abeaa7..5e8b8cd 100644 --- a/src/main/java/ru/practicum/shareit/request/model/ItemRequest.java +++ b/src/main/java/ru/practicum/shareit/request/model/ItemRequest.java @@ -1,7 +1,28 @@ package ru.practicum.shareit.request.model; +import jakarta.persistence.*; +import lombok.*; + /** * TODO Sprint add-item-requests. */ +@Getter +@Setter +@ToString +@Entity +@Table(name = "items") +@NoArgsConstructor +@AllArgsConstructor +@Builder(toBuilder = true) public class ItemRequest { -} + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "description") + private String description; + + @Column(name = "requestor_id") + private long requestorId; +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/user/controller/UserController.java b/src/main/java/ru/practicum/shareit/user/UserController.java similarity index 62% rename from src/main/java/ru/practicum/shareit/user/controller/UserController.java rename to src/main/java/ru/practicum/shareit/user/UserController.java index ba53c47..c1e579c 100644 --- a/src/main/java/ru/practicum/shareit/user/controller/UserController.java +++ b/src/main/java/ru/practicum/shareit/user/UserController.java @@ -1,21 +1,17 @@ -package ru.practicum.shareit.user.controller; +package ru.practicum.shareit.user; -import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.*; import ru.practicum.shareit.user.dto.UserDto; -import ru.practicum.shareit.user.service.UserService; +import ru.practicum.shareit.user.dto.UserNewDto; import java.util.Collection; -/** - * TODO Sprint add-controllers. - */ @Slf4j @RestController @RequiredArgsConstructor -@RequestMapping(path = "/users") +@RequestMapping("/users") public class UserController { private final UserService userService; @@ -26,27 +22,27 @@ public Collection findAll() { return userService.findAll(); } - @GetMapping("/{id}") - public UserDto findById(@NonNull @PathVariable("id") Long userId) { + @GetMapping("/{userId}") + public UserDto findById(@PathVariable Long userId) { log.info("Запрос на получение пользователя с Id:{}", userId); return userService.findById(userId); } @PostMapping - public UserDto create(@NonNull @RequestBody UserDto newUser) { + public UserDto create(@RequestBody UserNewDto newUser) { log.info("Запрос на добавление нового пользователя"); return userService.create(newUser); } - @PatchMapping("/{id}") - public UserDto update(@NonNull @PathVariable("id") Long userId, - @NonNull @RequestBody UserDto updatedUser) { - log.info("Запрос на обновление пользователя с id:{}", updatedUser.getId()); + @PatchMapping("/{userId}") + public UserDto update(@PathVariable Long userId, + @RequestBody UserNewDto updatedUser) { + log.info("Запрос на обновление пользователя с id:{}", userId); return userService.update(userId, updatedUser); } - @DeleteMapping("/{id}") - public void delete(@NonNull @PathVariable("id") Long userId) { + @DeleteMapping("/{userId}") + public void delete(@PathVariable Long userId) { log.info("Запрос на удаление пользователя с id:{}", userId); userService.delete(userId); } diff --git a/src/main/java/ru/practicum/shareit/user/service/UserService.java b/src/main/java/ru/practicum/shareit/user/UserService.java similarity index 54% rename from src/main/java/ru/practicum/shareit/user/service/UserService.java rename to src/main/java/ru/practicum/shareit/user/UserService.java index 6fa8bd2..c25f390 100644 --- a/src/main/java/ru/practicum/shareit/user/service/UserService.java +++ b/src/main/java/ru/practicum/shareit/user/UserService.java @@ -1,6 +1,7 @@ -package ru.practicum.shareit.user.service; +package ru.practicum.shareit.user; import ru.practicum.shareit.user.dto.UserDto; +import ru.practicum.shareit.user.dto.UserNewDto; import java.util.Collection; @@ -9,9 +10,9 @@ public interface UserService { UserDto findById(Long userId); - UserDto create(UserDto newUser); + UserDto create(UserNewDto newUser); - UserDto update(Long userId, UserDto updatedUser); + UserDto update(Long userId, UserNewDto updatedUser); void delete(Long userId); } \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/user/service/UserServiceImpl.java b/src/main/java/ru/practicum/shareit/user/UserServiceImpl.java similarity index 66% rename from src/main/java/ru/practicum/shareit/user/service/UserServiceImpl.java rename to src/main/java/ru/practicum/shareit/user/UserServiceImpl.java index 3476523..74c0012 100644 --- a/src/main/java/ru/practicum/shareit/user/service/UserServiceImpl.java +++ b/src/main/java/ru/practicum/shareit/user/UserServiceImpl.java @@ -1,4 +1,4 @@ -package ru.practicum.shareit.user.service; +package ru.practicum.shareit.user; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -6,11 +6,13 @@ import ru.practicum.shareit.exception.*; import ru.practicum.shareit.user.dao.UserRepository; import ru.practicum.shareit.user.dto.UserDto; +import ru.practicum.shareit.user.dto.UserNewDto; import ru.practicum.shareit.user.mapper.UserMapper; import ru.practicum.shareit.user.model.User; import ru.practicum.shareit.user.utils.UserValidate; import java.util.Collection; +import java.util.stream.Collectors; @Slf4j @Service @@ -24,7 +26,7 @@ public class UserServiceImpl implements UserService { public Collection findAll() { return userRepository.findAll().stream() .map(UserMapper::mapToDto) - .toList(); + .collect(Collectors.toList()); } // Получение пользователя по id @@ -37,48 +39,47 @@ public UserDto findById(Long userId) { // Добавление пользователя @Override - public UserDto create(UserDto userDto) { - User user = UserMapper.mapToUser(userDto); - UserValidate.validateForCreate(user); - checkUniqueEmail(userDto.getEmail()); - - User createdUser = userRepository.create(user); + public UserDto create(UserNewDto userNewDto) { + UserValidate.validateForCreate(userNewDto); + checkUniqueEmail(userNewDto.getEmail()); + User user = UserMapper.mapToUser(userNewDto); + User createdUser = userRepository.save(user); return UserMapper.mapToDto(createdUser); } // Обновление пользователя @Override - public UserDto update(Long userId, UserDto userDto) { + public UserDto update(Long userId, UserNewDto userNewDto) { if (userId == null || userId <= 0) { throw new ValidationException("Некорректный ID пользователя"); } - if (userDto == null) { - throw new ValidationException("Пользователь не может быть null"); - } + UserValidate.validateForUpdate(userNewDto); User existingUser = userRepository.findById(userId) .orElseThrow(() -> new NotFoundException("Пользователь с ID " + userId + " не найден")); - if (userDto.getName() != null) { - existingUser.setName(userDto.getName()); + if (userNewDto.getName() != null) { + existingUser.setName(userNewDto.getName()); } - if (userDto.getEmail() != null && !userDto.getEmail().equalsIgnoreCase(existingUser.getEmail())) { - checkUniqueEmail(userDto.getEmail()); - existingUser.setEmail(userDto.getEmail()); + if (userNewDto.getEmail() != null && !userNewDto.getEmail().equalsIgnoreCase(existingUser.getEmail())) { + checkUniqueEmail(userNewDto.getEmail()); + existingUser.setEmail(userNewDto.getEmail()); } - UserValidate.validateForEdit(existingUser); - - User updatedUser = userRepository.update(userId, existingUser); + User updatedUser = userRepository.save(existingUser); return UserMapper.mapToDto(updatedUser); } // Удаление пользователя @Override public void delete(Long userId) { - userRepository.delete(userId); + if (!userRepository.existsById(userId)) { + throw new NotFoundException("Пользователь с Id:" + userId + "не найден"); + } + + userRepository.deleteById(userId); } // Проверки уникальности email diff --git a/src/main/java/ru/practicum/shareit/user/dao/UserRepository.java b/src/main/java/ru/practicum/shareit/user/dao/UserRepository.java index 4e4d535..896e052 100644 --- a/src/main/java/ru/practicum/shareit/user/dao/UserRepository.java +++ b/src/main/java/ru/practicum/shareit/user/dao/UserRepository.java @@ -1,18 +1,10 @@ package ru.practicum.shareit.user.dao; +import org.springframework.data.jpa.repository.JpaRepository; import ru.practicum.shareit.user.model.User; -import java.util.Collection; import java.util.Optional; -public interface UserRepository { - Collection findAll(); - - Optional findById(Long userId); - - User create(User user); - - User update(Long userId, User user); - - void delete(Long userId); +public interface UserRepository extends JpaRepository { + Optional findByEmail(String email); } \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/user/dao/UserRepositoryImpl.java b/src/main/java/ru/practicum/shareit/user/dao/UserRepositoryImpl.java deleted file mode 100644 index 6343bbc..0000000 --- a/src/main/java/ru/practicum/shareit/user/dao/UserRepositoryImpl.java +++ /dev/null @@ -1,46 +0,0 @@ -package ru.practicum.shareit.user.dao; - -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Repository; -import ru.practicum.shareit.user.model.User; - -import java.util.*; - -@Slf4j -@Repository -public class UserRepositoryImpl implements UserRepository { - - private final Map users = new HashMap<>(); - private final Set usersEmail = new HashSet<>(); - private long idCounter = 0L; - - @Override - public Collection findAll() { - return List.copyOf(users.values()); - } - - @Override - public Optional findById(Long userId) { - return Optional.ofNullable(users.get(userId)); - } - - @Override - public User create(User user) { - user.setUserId(++idCounter); - usersEmail.add(user.getEmail().toLowerCase()); - users.put(user.getUserId(), user); - return user; - } - - @Override - public User update(Long userId, User user) { - users.put(user.getUserId(), user); - return user; - } - - @Override - public void delete(Long userId) { - usersEmail.remove(users.get(userId).getEmail()); - users.remove(userId); - } -} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/user/dto/UserDto.java b/src/main/java/ru/practicum/shareit/user/dto/UserDto.java index cfbdbf3..93c9e28 100644 --- a/src/main/java/ru/practicum/shareit/user/dto/UserDto.java +++ b/src/main/java/ru/practicum/shareit/user/dto/UserDto.java @@ -1,9 +1,11 @@ package ru.practicum.shareit.user.dto; import lombok.Builder; -import lombok.Data; +import lombok.Getter; +import lombok.Setter; -@Data +@Getter +@Setter @Builder(toBuilder = true) public class UserDto { private Long id; diff --git a/src/main/java/ru/practicum/shareit/user/dto/UserNewDto.java b/src/main/java/ru/practicum/shareit/user/dto/UserNewDto.java new file mode 100644 index 0000000..1825f4c --- /dev/null +++ b/src/main/java/ru/practicum/shareit/user/dto/UserNewDto.java @@ -0,0 +1,14 @@ +package ru.practicum.shareit.user.dto; + +import lombok.*; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@Builder(toBuilder = true) +public class UserNewDto { + + private String name; + private String email; +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/user/mapper/UserMapper.java b/src/main/java/ru/practicum/shareit/user/mapper/UserMapper.java index 130cba6..3509daa 100644 --- a/src/main/java/ru/practicum/shareit/user/mapper/UserMapper.java +++ b/src/main/java/ru/practicum/shareit/user/mapper/UserMapper.java @@ -1,6 +1,7 @@ package ru.practicum.shareit.user.mapper; import ru.practicum.shareit.user.dto.UserDto; +import ru.practicum.shareit.user.dto.UserNewDto; import ru.practicum.shareit.user.model.User; public class UserMapper { @@ -8,18 +9,17 @@ public class UserMapper { // Преобразование User в Dto public static UserDto mapToDto(User user) { return UserDto.builder() - .id(user.getUserId()) + .id(user.getId()) .name(user.getName()) .email(user.getEmail()) .build(); } // Преобразование Dto в User - public static User mapToUser(UserDto userDto) { + public static User mapToUser(UserNewDto userNewDto) { return User.builder() - .userId(userDto.getId()) - .name(userDto.getName()) - .email(userDto.getEmail()) + .name(userNewDto.getName()) + .email(userNewDto.getEmail()) .build(); } } \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/user/model/User.java b/src/main/java/ru/practicum/shareit/user/model/User.java index a1b5375..ffabf45 100644 --- a/src/main/java/ru/practicum/shareit/user/model/User.java +++ b/src/main/java/ru/practicum/shareit/user/model/User.java @@ -1,15 +1,43 @@ package ru.practicum.shareit.user.model; -import lombok.Builder; -import lombok.Data; +import jakarta.persistence.*; +import lombok.*; -/** - * TODO Sprint add-controllers. - */ -@Data +@Getter +@Setter +@ToString +@Entity +@Table(name = "users", schema = "public") +@NoArgsConstructor +@AllArgsConstructor @Builder(toBuilder = true) public class User { - private Long userId; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "name", nullable = false) private String name; + + @Column(name = "email", nullable = false, unique = true) private String email; + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (!(o instanceof User)) { + return false; + } + + return id != null && id.equals(((User) o).getId()); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } } \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/user/utils/UserValidate.java b/src/main/java/ru/practicum/shareit/user/utils/UserValidate.java index fc2e208..ac415d6 100644 --- a/src/main/java/ru/practicum/shareit/user/utils/UserValidate.java +++ b/src/main/java/ru/practicum/shareit/user/utils/UserValidate.java @@ -4,7 +4,7 @@ import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; import ru.practicum.shareit.exception.ValidationException; -import ru.practicum.shareit.user.model.User; +import ru.practicum.shareit.user.dto.UserNewDto; /** * Утилитарный класс для валидации данных сущности User @@ -15,17 +15,29 @@ public class UserValidate { // Валидация при создании пользователя - public static void validateForCreate(User user) { - validateNotNull(user); - validateEmail(user.getEmail()); + public static void validateForCreate(UserNewDto userNewDto) { + validateNotNull(userNewDto); + validateName(userNewDto.getName()); + validateEmail(userNewDto.getEmail()); } // Валидация при изменении пользователя - public static void validateForEdit(User user) { - validateNotNull(user); + public static void validateForUpdate(UserNewDto userNewDto) { + validateNotNull(userNewDto); - if (user.getEmail() != null) { - validateEmail(user.getEmail()); + if (userNewDto.getName() != null) { + validateName(userNewDto.getName()); + } + + if (userNewDto.getEmail() != null) { + validateEmail(userNewDto.getEmail()); + } + } + + // Вспомогательный метод валидации имени + private static void validateName(String name) { + if (name == null || name.isBlank()) { + throw new ValidationException("Имя пользователя не может быть пустым"); } } @@ -42,8 +54,8 @@ private static void validateEmail(String email) { } // Вспомогательный метод проверки пользователя на null - private static void validateNotNull(User user) { - if (user == null) { + private static void validateNotNull(UserNewDto userNewDto) { + if (userNewDto == null) { log.error("Попытка добавить или изменить null, а не пользователя"); throw new ValidationException("Пользователь не может быть null"); } diff --git a/src/main/resources/application-test.properties b/src/main/resources/application-test.properties index 9e9bc4b..71caf05 100644 --- a/src/main/resources/application-test.properties +++ b/src/main/resources/application-test.properties @@ -7,7 +7,7 @@ logging.level.org.springframework.transaction.interceptor=TRACE logging.level.org.springframework.orm.jpa.JpaTransactionManager=DEBUG # TODO Append connection to H2 DB -#spring.datasource.driverClassName -#spring.datasource.url -#spring.datasource.username -#spring.datasource.password +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.url=jdbc:h2:mem:shareit;MODE=PostgreSQL +spring.datasource.username=sa +spring.datasource.password=password \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 51c5180..713d82b 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,14 +1,13 @@ +server.port=8080 spring.jpa.hibernate.ddl-auto=none spring.jpa.properties.hibernate.format_sql=true spring.sql.init.mode=always - logging.level.org.springframework.orm.jpa=INFO logging.level.org.springframework.transaction=INFO logging.level.org.springframework.transaction.interceptor=TRACE logging.level.org.springframework.orm.jpa.JpaTransactionManager=DEBUG -# TODO Append connection to Postgres DB -#spring.datasource.driverClassName -#spring.datasource.url -#spring.datasource.username -#spring.datasource.password +spring.datasource.driverClassName=org.postgresql.Driver +spring.datasource.url=jdbc:postgresql://localhost:5432/shareit +spring.datasource.username=dbuser +spring.datasource.password=12345 \ No newline at end of file diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql new file mode 100644 index 0000000..62c9bd7 --- /dev/null +++ b/src/main/resources/schema.sql @@ -0,0 +1,45 @@ +CREATE TABLE IF NOT EXISTS users ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + name VARCHAR(100) NOT NULL, + email VARCHAR(100) NOT NULL UNIQUE +); + +CREATE TABLE IF NOT EXISTS requests ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + description VARCHAR(250), + requester_id BIGINT NOT NULL, + FOREIGN KEY (requester_id) REFERENCES users (id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS items ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + name VARCHAR(100) NOT NULL, + description VARCHAR(500), + is_available BOOLEAN NOT NULL, + owner_id BIGINT NOT NULL, + request_id BIGINT, + FOREIGN KEY (owner_id) REFERENCES users (id) ON DELETE CASCADE, + FOREIGN KEY (request_id) REFERENCES requests (id) ON DELETE SET NULL +); + +CREATE TABLE IF NOT EXISTS bookings ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + start_date TIMESTAMP WITHOUT TIME ZONE NOT NULL, + end_date TIMESTAMP WITHOUT TIME ZONE NOT NULL, + item_id BIGINT NOT NULL, + booker_id BIGINT NOT NULL, + status VARCHAR(30) NOT NULL, + + FOREIGN KEY (item_id) REFERENCES items (id) ON DELETE CASCADE, + FOREIGN KEY (booker_id) REFERENCES users (id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS comments ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + text VARCHAR(1000) NOT NULL, + item_id BIGINT NOT NULL, + author_id BIGINT NOT NULL, + created TIMESTAMP WITHOUT TIME ZONE NOT NULL, + FOREIGN KEY (item_id) REFERENCES items (id) ON DELETE CASCADE, + FOREIGN KEY (author_id) REFERENCES users (id) ON DELETE CASCADE +); \ No newline at end of file diff --git a/src/test/java/ru/practicum/shareit/ShareItTests.java b/src/test/java/ru/practicum/shareit/ShareItTests.java index 4d79052..8cb88ed 100644 --- a/src/test/java/ru/practicum/shareit/ShareItTests.java +++ b/src/test/java/ru/practicum/shareit/ShareItTests.java @@ -9,5 +9,4 @@ class ShareItTests { @Test void contextLoads() { } - -} +} \ No newline at end of file