diff --git a/.gitignore b/.gitignore index 549e00a..e45a4b8 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,8 @@ build/ ### VS Code ### .vscode/ + +shareIt.mv.db +shareIt.trace.db +db/shareIt.mv.db +db/*.db \ No newline at end of file diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..35ebf89 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,17 @@ +services: + db: + image: postgres:16.1 + container_name: postgres5 + ports: + - "5438:5432" + volumes: + - ./volumes/postgres:/var/lib/postgresql/data/ + environment: + - POSTGRES_DB=shareitdb + - POSTGRES_USER=dbuser + - POSTGRES_PASSWORD=12345 + healthcheck: + test: pg_isready -q -d $$POSTGRES_DB -U $$POSTGRES_USER + timeout: 5s + interval: 5s + retries: 10 \ No newline at end of file diff --git a/pom.xml b/pom.xml index 2db888c..b1f2886 100644 --- a/pom.xml +++ b/pom.xml @@ -1,247 +1,251 @@ - 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.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-configuration-processor + true + - - org.postgresql - postgresql - runtime - + + org.projectlombok + lombok + true + - - org.projectlombok - lombok - true - + + com.h2database + h2 + runtime + - - com.h2database - h2 - test - - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.boot - spring-boot-starter-validation - - + + org.postgresql + postgresql + runtime + + + 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-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 - - - - - + + + + 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 + + + + + - + \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/booking/Booking.java b/src/main/java/ru/practicum/shareit/booking/Booking.java deleted file mode 100644 index f48b024..0000000 --- a/src/main/java/ru/practicum/shareit/booking/Booking.java +++ /dev/null @@ -1,22 +0,0 @@ -package ru.practicum.shareit.booking; - -import lombok.AllArgsConstructor; -import lombok.Data; -import ru.practicum.shareit.item.model.Item; -import ru.practicum.shareit.user.model.User; - -import java.time.LocalDateTime; - -/** - * TODO Sprint add-bookings. - */ -@AllArgsConstructor -@Data -public class Booking { - private Long id; - private LocalDateTime bookingStartDate; - private LocalDateTime endDateOfBooking; - private Item item; - private User booker; - private RentalStatus rentalStatus; -} diff --git a/src/main/java/ru/practicum/shareit/booking/BookingController.java b/src/main/java/ru/practicum/shareit/booking/BookingController.java deleted file mode 100644 index b94493d..0000000 --- a/src/main/java/ru/practicum/shareit/booking/BookingController.java +++ /dev/null @@ -1,12 +0,0 @@ -package ru.practicum.shareit.booking; - -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/controller/BookingController.java b/src/main/java/ru/practicum/shareit/booking/controller/BookingController.java new file mode 100644 index 0000000..a6a71d7 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/controller/BookingController.java @@ -0,0 +1,49 @@ +package ru.practicum.shareit.booking.controller; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; +import ru.practicum.shareit.booking.dto.BookingRequestDto; +import ru.practicum.shareit.booking.dto.BookingResponseDto; +import ru.practicum.shareit.booking.service.BookingService; + +import java.util.List; + +@RequiredArgsConstructor +@Slf4j +@RestController +@RequestMapping(path = "/bookings") +public class BookingController { + private final BookingService bookingService; + + @PostMapping + public BookingResponseDto createBooking(@RequestHeader("X-Sharer-User-Id") Long userId, @Valid @RequestBody BookingRequestDto bookingRequestDto) { + log.info("POST /bookings"); + return bookingService.createBooking(userId, bookingRequestDto); + } + + @PatchMapping("/{bookingId}") + public BookingResponseDto updateBooking(@RequestHeader("X-Sharer-User-Id") Long userId, @PathVariable("bookingId") Long bookingId, @RequestParam(name = "approved") Boolean approved) { + log.info("PATCH /bookings/{}", bookingId); + return bookingService.updateBooking(userId, bookingId, approved); + } + + @GetMapping("/{bookingId}") + public BookingResponseDto findBookingById(@RequestHeader("X-Sharer-User-Id") Long userId, @PathVariable("bookingId") Long bookingId) { + log.info("GET /bookings/{}", bookingId); + return bookingService.findBookingById(userId, bookingId); + } + + @GetMapping + public List findUserBookings(@RequestHeader("X-Sharer-User-Id") Long userId, @RequestParam(name = "state", defaultValue = "ALL") String state) { + log.info("GET /bookings?state={}.", state); + return bookingService.findUserBookings(userId, state); + } + + @GetMapping("/owner") + public List findOwnerReservedItems(@RequestHeader("X-Sharer-User-Id") Long userId, @RequestParam(name = "state", defaultValue = "ALL") String state) { + log.info("GET /bookings/owner?state={}.", state); + return bookingService.findOwnerReservedItems(userId, state); + } +} diff --git a/src/main/java/ru/practicum/shareit/booking/dto/BookingDto.java b/src/main/java/ru/practicum/shareit/booking/dto/BookingDto.java deleted file mode 100644 index 861de9e..0000000 --- a/src/main/java/ru/practicum/shareit/booking/dto/BookingDto.java +++ /dev/null @@ -1,7 +0,0 @@ -package ru.practicum.shareit.booking.dto; - -/** - * TODO Sprint add-bookings. - */ -public class BookingDto { -} diff --git a/src/main/java/ru/practicum/shareit/booking/dto/BookingRequestDto.java b/src/main/java/ru/practicum/shareit/booking/dto/BookingRequestDto.java new file mode 100644 index 0000000..e311cde --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/dto/BookingRequestDto.java @@ -0,0 +1,24 @@ +package ru.practicum.shareit.booking.dto; + +import jakarta.validation.constraints.Future; +import jakarta.validation.constraints.FutureOrPresent; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDateTime; + +@AllArgsConstructor +@Getter +@Setter +public class BookingRequestDto { + @NotNull + private Long itemId; + @FutureOrPresent + @NotNull + private LocalDateTime start; + @Future + @NotNull + private LocalDateTime end; +} diff --git a/src/main/java/ru/practicum/shareit/booking/dto/BookingResponseDto.java b/src/main/java/ru/practicum/shareit/booking/dto/BookingResponseDto.java new file mode 100644 index 0000000..334abb1 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/dto/BookingResponseDto.java @@ -0,0 +1,25 @@ +package ru.practicum.shareit.booking.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import ru.practicum.shareit.booking.status.RentalStatus; +import ru.practicum.shareit.item.dto.ItemDto; +import ru.practicum.shareit.user.dto.UserDto; + +import java.time.LocalDateTime; + +@AllArgsConstructor +@Getter +@Setter +public class BookingResponseDto { + private Long id; + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") + private LocalDateTime start; + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") + private LocalDateTime end; + private ItemDto item; + private UserDto booker; + private RentalStatus status; +} diff --git a/src/main/java/ru/practicum/shareit/booking/dto/SimplifiedBookingDto.java b/src/main/java/ru/practicum/shareit/booking/dto/SimplifiedBookingDto.java new file mode 100644 index 0000000..07909fa --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/dto/SimplifiedBookingDto.java @@ -0,0 +1,13 @@ +package ru.practicum.shareit.booking.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +@AllArgsConstructor +@Getter +@Setter +public class SimplifiedBookingDto { + private Long id; + private Long bookerId; +} diff --git a/src/main/java/ru/practicum/shareit/booking/dto/mapper/BookingMapper.java b/src/main/java/ru/practicum/shareit/booking/dto/mapper/BookingMapper.java new file mode 100644 index 0000000..e015d27 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/dto/mapper/BookingMapper.java @@ -0,0 +1,57 @@ +package ru.practicum.shareit.booking.dto.mapper; + +import lombok.experimental.UtilityClass; +import ru.practicum.shareit.booking.dto.BookingRequestDto; +import ru.practicum.shareit.booking.dto.BookingResponseDto; +import ru.practicum.shareit.booking.dto.SimplifiedBookingDto; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.status.RentalStatus; +import ru.practicum.shareit.item.dto.ItemDto; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.user.dto.UserDto; +import ru.practicum.shareit.user.model.User; + +@UtilityClass +public class BookingMapper { + public static BookingResponseDto toBookingDto(Booking booking) { + ItemDto itemDto = new ItemDto(booking.getItem().getId(), + booking.getItem().getName(), + booking.getItem().getDescription(), + booking.getItem().getAvailable() + ); + + UserDto bookerDto = new UserDto( + booking.getBooker().getId(), + booking.getBooker().getName(), + booking.getBooker().getEmail() + ); + + return new BookingResponseDto( + booking.getId(), + booking.getStart(), + booking.getEnd(), + itemDto, + bookerDto, + booking.getStatus() + ); + } + + public static Booking toBooking(BookingRequestDto bookingRequestDto, Item item, User booker, RentalStatus status) { + return new Booking( + null, + bookingRequestDto.getStart(), + bookingRequestDto.getEnd(), + item, + booker, + status + ); + } + + public static SimplifiedBookingDto toSimplifiedBookingDto(Booking booking) { + if (booking == null) return null; + return new SimplifiedBookingDto( + booking.getId(), + booking.getBooker().getId() + ); + } +} diff --git a/src/main/java/ru/practicum/shareit/booking/model/Booking.java b/src/main/java/ru/practicum/shareit/booking/model/Booking.java new file mode 100644 index 0000000..e7c3816 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/model/Booking.java @@ -0,0 +1,33 @@ +package ru.practicum.shareit.booking.model; + +import jakarta.persistence.*; +import lombok.*; +import ru.practicum.shareit.booking.status.RentalStatus; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.user.model.User; + +import java.time.LocalDateTime; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@Entity +@Table(name = "bookings") +public class Booking { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @Column(name = "start_date") + private LocalDateTime start; + @Column(name = "end_date") + private LocalDateTime 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) + private RentalStatus status; +} diff --git a/src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java b/src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java new file mode 100644 index 0000000..51c9335 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java @@ -0,0 +1,49 @@ +package ru.practicum.shareit.booking.repository; + +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.status.RentalStatus; +import ru.practicum.shareit.user.model.User; + +import java.time.LocalDateTime; +import java.util.List; + +public interface BookingRepository extends JpaRepository { + @Query("SELECT b FROM Booking AS b WHERE b.booker = :booker AND CURRENT_TIMESTAMP BETWEEN b.start AND b.end") + List findCurrentBookings(@Param("booker") User booker, Sort sort); + + @Query("SELECT b FROM Booking AS b WHERE b.booker = :booker AND b.end < CURRENT_TIMESTAMP") + List findPastBookings(@Param("booker") User booker, Sort sort); + + @Query("SELECT b FROM Booking AS b WHERE b.booker = :booker AND b.start > CURRENT_TIMESTAMP") + List findFutureBookings(@Param("booker") User booker, Sort sort); + + List findByBookerAndStatus(User booker, RentalStatus status, Sort sort); + + List findByBooker(User booker, Sort sort); + + @Query("SELECT b FROM Booking AS b WHERE b.item.owner = :owner AND CURRENT_TIMESTAMP BETWEEN b.start AND b.end") + List findCurrentItemsBookings(@Param("owner") User owner, Sort sort); + + @Query("SELECT b FROM Booking AS b WHERE b.item.owner = :owner AND b.end < CURRENT_TIMESTAMP") + List findPastItemsBookings(@Param("owner") User owner, Sort sort); + + @Query("SELECT b FROM Booking AS b WHERE b.item.owner = :owner AND b.start > CURRENT_TIMESTAMP") + List findFutureItemsBookings(@Param("owner") User owner, Sort sort); + + List findByItemOwnerAndStatus(User owner, RentalStatus status, Sort sort); + + @Query("SELECT b FROM Booking AS b WHERE b.item.owner = :owner") + List findByItemOwner(@Param("owner") User owner, Sort sort); + + Booking findFirstByBookerIdAndItemIdAndEndIsBefore(Long bookerId, Long itemId, LocalDateTime end, Sort sort); + + @Query("SELECT b FROM Booking b WHERE b.item.id IN :itemIds AND b.end <= CURRENT_TIMESTAMP") + List findLastBookingsForItems(@Param("itemIds") List itemIds, Sort sort); + + @Query("SELECT b FROM Booking b WHERE b.item.id IN :itemIds AND b.start >= CURRENT_TIMESTAMP") + List findNextBookingsForItems(@Param("itemIds") List itemIds, Sort sort); +} diff --git a/src/main/java/ru/practicum/shareit/booking/service/BookingService.java b/src/main/java/ru/practicum/shareit/booking/service/BookingService.java new file mode 100644 index 0000000..9fb6c87 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/service/BookingService.java @@ -0,0 +1,18 @@ +package ru.practicum.shareit.booking.service; + +import ru.practicum.shareit.booking.dto.BookingRequestDto; +import ru.practicum.shareit.booking.dto.BookingResponseDto; + +import java.util.List; + +public interface BookingService { + BookingResponseDto createBooking(Long userId, BookingRequestDto bookingRequestDto); + + BookingResponseDto updateBooking(Long userId, Long bookingId, Boolean approved); + + BookingResponseDto findBookingById(Long userId, Long bookingId); + + List findUserBookings(Long userId, String state); + + List findOwnerReservedItems(Long userId, String state); +} diff --git a/src/main/java/ru/practicum/shareit/booking/service/BookingServiceImpl.java b/src/main/java/ru/practicum/shareit/booking/service/BookingServiceImpl.java new file mode 100644 index 0000000..327df26 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/service/BookingServiceImpl.java @@ -0,0 +1,176 @@ +package ru.practicum.shareit.booking.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import ru.practicum.shareit.booking.dto.BookingRequestDto; +import ru.practicum.shareit.booking.dto.BookingResponseDto; +import ru.practicum.shareit.booking.dto.mapper.BookingMapper; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.repository.BookingRepository; +import ru.practicum.shareit.booking.status.RentalStatus; +import ru.practicum.shareit.exception.NotFoundException; +import ru.practicum.shareit.exception.ValidationException; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.item.repository.ItemRepository; +import ru.practicum.shareit.user.model.User; +import ru.practicum.shareit.user.repository.UserRepository; + +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +@RequiredArgsConstructor +@Slf4j +@Service +public class BookingServiceImpl implements BookingService { + private final BookingRepository bookingRepository; + private final UserRepository userRepository; + private final ItemRepository itemRepository; + + @Override + public BookingResponseDto createBooking(Long userId, BookingRequestDto bookingRequestDto) { + User booker = validateUser(userId); + Item item = validateItem(bookingRequestDto.getItemId()); + + if (Objects.equals(item.getOwner().getId(), userId)) { + log.warn("Владелец не может бронировать свою вещь"); + throw new ValidationException("Владелец не может бронировать свою вещь"); + } + if (!item.getAvailable()) { + log.warn("Данная вещь недоступна для бронирования"); + throw new ValidationException("Данная вещь недоступна для бронирования"); + } + + if (!bookingRequestDto.getStart().isBefore(bookingRequestDto.getEnd())) { + log.warn("Период указан неверно"); + throw new ValidationException("Период указан неверно"); + } + + RentalStatus status = RentalStatus.WAITING; + + return BookingMapper.toBookingDto(bookingRepository.save(BookingMapper.toBooking(bookingRequestDto, item, booker, status))); + } + + @Override + public BookingResponseDto updateBooking(Long userId, Long bookingId, Boolean approved) { + validateForbiddenUser(userId); + Booking booking = validateBooking(bookingId); + + if (!Objects.equals(userId, booking.getItem().getOwner().getId())) { + log.warn("Обновить аренду может только владелец вещи"); + throw new ValidationException("Обновить аренду может только владелец вещи"); + } + + if (!Objects.equals(booking.getStatus(), RentalStatus.WAITING)) { + log.warn("Статус уже был установлен"); + throw new ValidationException("Нельзя изменить статус: бронирование уже подтверждено или отклонено"); + } + + booking.setStatus(approved ? RentalStatus.APPROVED : RentalStatus.REJECTED); + + return BookingMapper.toBookingDto(bookingRepository.save(booking)); + } + + @Override + public BookingResponseDto findBookingById(Long userId, Long bookingId) { + validateUser(userId); + Booking booking = validateBooking(bookingId); + + if (!Objects.equals(booking.getItem().getOwner().getId(), userId) && !Objects.equals(booking.getBooker().getId(), userId)) { + log.warn("Данный пользователь не может получить информацию по бронированию"); + throw new ValidationException("Данный пользователь не может получить информацию по бронированию"); + } + + return BookingMapper.toBookingDto(booking); + } + + @Override + public List findUserBookings(Long userId, String state) { + User booker = validateUser(userId); + + List bookerBookings; + Sort sortDescByStart = Sort.by(Sort.Direction.DESC, "start"); + + switch (state) { + case "CURRENT": + bookerBookings = bookingRepository.findCurrentBookings(booker, sortDescByStart); + break; + case "PAST": + bookerBookings = bookingRepository.findPastBookings(booker, sortDescByStart); + break; + case "FUTURE": + bookerBookings = bookingRepository.findFutureBookings(booker, sortDescByStart); + break; + case "WAITING": + bookerBookings = bookingRepository.findByBookerAndStatus(booker, RentalStatus.WAITING, sortDescByStart); + break; + case "REJECTED": + bookerBookings = bookingRepository.findByBookerAndStatus(booker, RentalStatus.REJECTED, sortDescByStart); + break; + case "ALL": + default: + bookerBookings = bookingRepository.findByBooker(booker, sortDescByStart); + break; + } + + return bookerBookings.stream() + .map(BookingMapper::toBookingDto) + .collect(Collectors.toList()); + } + + @Override + public List findOwnerReservedItems(Long userId, String state) { + User owner = validateUser(userId); + + List ownerBookings; + Sort sortDescByStart = Sort.by(Sort.Direction.DESC, "start"); + + switch (state) { + case "CURRENT": + ownerBookings = bookingRepository.findCurrentItemsBookings(owner, sortDescByStart); + break; + case "PAST": + ownerBookings = bookingRepository.findPastItemsBookings(owner, sortDescByStart); + break; + case "FUTURE": + ownerBookings = bookingRepository.findFutureItemsBookings(owner, sortDescByStart); + break; + case "WAITING": + ownerBookings = bookingRepository.findByItemOwnerAndStatus(owner, RentalStatus.WAITING, sortDescByStart); + break; + case "REJECTED": + ownerBookings = bookingRepository.findByItemOwnerAndStatus(owner, RentalStatus.REJECTED, sortDescByStart); + break; + case "ALL": + default: + ownerBookings = bookingRepository.findByItemOwner(owner, sortDescByStart); + break; + } + + return ownerBookings.stream() + .map(BookingMapper::toBookingDto) + .collect(Collectors.toList()); + } + + private void validateForbiddenUser(Long userId) { + userRepository.findById(userId) + .orElseThrow(() -> new ValidationException("Пользователю с данным id: " + userId + " доступ запрещен")); + } + + private User validateUser(Long userId) { + return userRepository.findById(userId) + .orElseThrow(() -> new NotFoundException("Пользователь с данным id: " + userId + " не найден")); + } + + private Item validateItem(Long itemId) { + return itemRepository.findById(itemId) + .orElseThrow(() -> new NotFoundException("Вещь с данным id: " + itemId + " не найден")); + } + + private Booking validateBooking(Long bookingId) { + return bookingRepository.findById(bookingId) + .orElseThrow(() -> new NotFoundException("Бронирование с данным id:" + bookingId + " не найдено")); + } +} diff --git a/src/main/java/ru/practicum/shareit/booking/RentalStatus.java b/src/main/java/ru/practicum/shareit/booking/status/RentalStatus.java similarity index 65% rename from src/main/java/ru/practicum/shareit/booking/RentalStatus.java rename to src/main/java/ru/practicum/shareit/booking/status/RentalStatus.java index 5d4afad..a36dfb1 100644 --- a/src/main/java/ru/practicum/shareit/booking/RentalStatus.java +++ b/src/main/java/ru/practicum/shareit/booking/status/RentalStatus.java @@ -1,4 +1,4 @@ -package ru.practicum.shareit.booking; +package ru.practicum.shareit.booking.status; public enum RentalStatus { WAITING, diff --git a/src/main/java/ru/practicum/shareit/error/ErrorResponse.java b/src/main/java/ru/practicum/shareit/error/ErrorResponse.java index 76b0cb2..637fb8c 100644 --- a/src/main/java/ru/practicum/shareit/error/ErrorResponse.java +++ b/src/main/java/ru/practicum/shareit/error/ErrorResponse.java @@ -5,7 +5,7 @@ @AllArgsConstructor @Getter -class ErrorResponse { +public class ErrorResponse { private final String error; private final String description; } diff --git a/src/main/java/ru/practicum/shareit/item/comment/dto/CommentRequestDto.java b/src/main/java/ru/practicum/shareit/item/comment/dto/CommentRequestDto.java new file mode 100644 index 0000000..7943bae --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/comment/dto/CommentRequestDto.java @@ -0,0 +1,15 @@ +package ru.practicum.shareit.item.comment.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.*; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +public class CommentRequestDto { + @Size(max = 2000, message = "Комментарий не может быть длиннее 2000 символов") + @NotBlank(message = "Комментарий не может быть пустым") + private String text; +} diff --git a/src/main/java/ru/practicum/shareit/item/comment/dto/CommentResponseDto.java b/src/main/java/ru/practicum/shareit/item/comment/dto/CommentResponseDto.java new file mode 100644 index 0000000..5708e07 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/comment/dto/CommentResponseDto.java @@ -0,0 +1,20 @@ +package ru.practicum.shareit.item.comment.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDateTime; + +@AllArgsConstructor +@Getter +@Setter +public class CommentResponseDto { + private Long id; + private String text; + private String authorName; + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") + private LocalDateTime created; + private Long itemId; +} diff --git a/src/main/java/ru/practicum/shareit/item/comment/dto/mapper/CommentMapper.java b/src/main/java/ru/practicum/shareit/item/comment/dto/mapper/CommentMapper.java new file mode 100644 index 0000000..09b3455 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/comment/dto/mapper/CommentMapper.java @@ -0,0 +1,33 @@ +package ru.practicum.shareit.item.comment.dto.mapper; + +import lombok.experimental.UtilityClass; +import ru.practicum.shareit.item.comment.dto.CommentRequestDto; +import ru.practicum.shareit.item.comment.dto.CommentResponseDto; +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.LocalDateTime; + +@UtilityClass +public class CommentMapper { + public static CommentResponseDto toCommentResponseDto(Comment comment) { + return new CommentResponseDto( + comment.getId(), + comment.getText(), + comment.getAuthor().getName(), + comment.getCreated(), + comment.getItem().getId() + ); + } + + public static Comment toComment(CommentRequestDto commentRequestDto, User author, Item item) { + return new Comment( + null, + commentRequestDto.getText(), + item, + author, + LocalDateTime.now() + ); + } +} 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..42371fb --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/comment/model/Comment.java @@ -0,0 +1,28 @@ +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.LocalDateTime; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@Entity +@Table(name = "comments") +public class Comment { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + 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; + private LocalDateTime created; +} diff --git a/src/main/java/ru/practicum/shareit/item/comment/repository/CommentRepository.java b/src/main/java/ru/practicum/shareit/item/comment/repository/CommentRepository.java new file mode 100644 index 0000000..59fad89 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/comment/repository/CommentRepository.java @@ -0,0 +1,10 @@ +package ru.practicum.shareit.item.comment.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import ru.practicum.shareit.item.comment.model.Comment; + +import java.util.List; + +public interface CommentRepository extends JpaRepository { + List findByItemIdIn(List itemIds); +} diff --git a/src/main/java/ru/practicum/shareit/item/controller/ItemController.java b/src/main/java/ru/practicum/shareit/item/controller/ItemController.java index 1f86c0f..a0293d0 100644 --- a/src/main/java/ru/practicum/shareit/item/controller/ItemController.java +++ b/src/main/java/ru/practicum/shareit/item/controller/ItemController.java @@ -4,6 +4,9 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.*; +import ru.practicum.shareit.item.comment.dto.CommentRequestDto; +import ru.practicum.shareit.item.comment.dto.CommentResponseDto; +import ru.practicum.shareit.item.dto.ItemBookingDto; import ru.practicum.shareit.item.dto.ItemDto; import ru.practicum.shareit.item.service.ItemService; @@ -29,13 +32,13 @@ public ItemDto updateItem(@RequestHeader("X-Sharer-User-Id") Long userId, @PathV } @GetMapping("/{itemId}") - public ItemDto findItemById(@PathVariable("itemId") Long itemId) { + public ItemBookingDto findItemById(@RequestHeader("X-Sharer-User-Id") Long userId, @PathVariable("itemId") Long itemId) { log.info("GET /items/{}", itemId); - return itemService.findItemById(itemId); + return itemService.findItemById(userId, itemId); } @GetMapping - public List findUserItems(@RequestHeader("X-Sharer-User-Id") Long userId) { + public List findUserItems(@RequestHeader("X-Sharer-User-Id") Long userId) { log.info("GET /items"); return itemService.findUserItems(userId); } @@ -45,4 +48,12 @@ public List searchItem(@RequestParam(name = "text") String text) { log.info("GET /items/search?text={}.", text); return itemService.searchItem(text); } + + @PostMapping("/{itemId}/comment") + public CommentResponseDto createComment(@RequestHeader("X-Sharer-User-Id") Long userId, + @PathVariable("itemId") Long itemId, + @Valid @RequestBody CommentRequestDto commentRequestDto) { + log.info("POST /items/{}/comment", itemId); + return itemService.createComment(userId, itemId, commentRequestDto); + } } diff --git a/src/main/java/ru/practicum/shareit/item/dao/ItemStorage.java b/src/main/java/ru/practicum/shareit/item/dao/ItemStorage.java deleted file mode 100644 index f539e57..0000000 --- a/src/main/java/ru/practicum/shareit/item/dao/ItemStorage.java +++ /dev/null @@ -1,17 +0,0 @@ -package ru.practicum.shareit.item.dao; - -import ru.practicum.shareit.item.model.Item; - -import java.util.List; - -public interface ItemStorage { - Item createItem(Item item); - - Item updateItem(Long userId, Item updateItem); - - Item findItemById(Long itemId); - - List findUserItems(); - - List searchItem(); -} diff --git a/src/main/java/ru/practicum/shareit/item/dao/ItemStorageImpl.java b/src/main/java/ru/practicum/shareit/item/dao/ItemStorageImpl.java deleted file mode 100644 index 3eef63e..0000000 --- a/src/main/java/ru/practicum/shareit/item/dao/ItemStorageImpl.java +++ /dev/null @@ -1,68 +0,0 @@ -package ru.practicum.shareit.item.dao; - -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Repository; -import ru.practicum.shareit.exception.NotFoundException; -import ru.practicum.shareit.item.model.Item; - -import java.util.*; - -@Slf4j -@Repository -public class ItemStorageImpl implements ItemStorage { - - private final Map items = new HashMap<>(); - - @Override - public Item createItem(Item item) { - item.setId(getId()); - items.put(item.getId(), item); - return item; - } - - @Override - public Item updateItem(Long userId, Item updateItem) { - if (!items.containsKey(updateItem.getId())) { - log.warn("Обновляемая вещь не найдена"); - throw new NotFoundException("Обновляемая вещь не найдена"); - } else { - Item oldItem = items.get(updateItem.getId()); - - if (updateItem.getName() == null) { - updateItem.setName(oldItem.getName()); - } - if (updateItem.getDescription() == null) { - updateItem.setDescription(oldItem.getDescription()); - } - if (updateItem.getAvailable() == null) { - updateItem.setAvailable(oldItem.getAvailable()); - } - } - - items.put(updateItem.getId(), updateItem); - return updateItem; - } - - @Override - public Item findItemById(Long itemId) { - return items.get(itemId); - } - - @Override - public List findUserItems() { - return new ArrayList<>(items.values()); - } - - @Override - public List searchItem() { - return new ArrayList<>(items.values()); - } - - private Long getId() { - long lastId = items.keySet().stream() - .mapToLong(Long::longValue) - .max() - .orElse(0); - return lastId + 1; - } -} diff --git a/src/main/java/ru/practicum/shareit/item/dto/ItemBookingDto.java b/src/main/java/ru/practicum/shareit/item/dto/ItemBookingDto.java new file mode 100644 index 0000000..3f91c0f --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/dto/ItemBookingDto.java @@ -0,0 +1,21 @@ +package ru.practicum.shareit.item.dto; + +import lombok.*; +import ru.practicum.shareit.booking.dto.SimplifiedBookingDto; +import ru.practicum.shareit.item.comment.dto.CommentResponseDto; + +import java.util.List; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +public class ItemBookingDto { + private Long id; + private String name; + private String description; + private Boolean available; + private SimplifiedBookingDto lastBooking; + private SimplifiedBookingDto nextBooking; + private List comments; +} 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 b17d740..a750b8a 100644 --- a/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java +++ b/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java @@ -3,10 +3,12 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; -import lombok.Data; +import lombok.Getter; +import lombok.Setter; @AllArgsConstructor -@Data +@Getter +@Setter public class ItemDto { private Long id; @NotBlank(message = "Имя не может быть null") diff --git a/src/main/java/ru/practicum/shareit/item/dto/mapper/ItemMapper.java b/src/main/java/ru/practicum/shareit/item/dto/mapper/ItemMapper.java index 5a3137c..72cbccd 100644 --- a/src/main/java/ru/practicum/shareit/item/dto/mapper/ItemMapper.java +++ b/src/main/java/ru/practicum/shareit/item/dto/mapper/ItemMapper.java @@ -1,10 +1,15 @@ package ru.practicum.shareit.item.dto.mapper; import lombok.experimental.UtilityClass; +import ru.practicum.shareit.booking.dto.SimplifiedBookingDto; +import ru.practicum.shareit.item.comment.dto.CommentResponseDto; +import ru.practicum.shareit.item.dto.ItemBookingDto; import ru.practicum.shareit.item.dto.ItemDto; import ru.practicum.shareit.item.model.Item; import ru.practicum.shareit.user.model.User; +import java.util.List; + @UtilityClass public class ItemMapper { public static ItemDto toItemDto(Item item) { @@ -22,19 +27,22 @@ public static Item toItem(User user, ItemDto itemDto) { itemDto.getName(), itemDto.getDescription(), itemDto.getAvailable(), - user, - null + user ); } - public static Item toItem(Long id, User user, ItemDto itemDto) { - return new Item( - id, - itemDto.getName(), - itemDto.getDescription(), - itemDto.getAvailable(), - user, - null + public static ItemBookingDto toItemBookingDto(Item item, + SimplifiedBookingDto lastBooking, + SimplifiedBookingDto nextBooking, + List comments) { + return new ItemBookingDto( + item.getId(), + item.getName(), + item.getDescription(), + item.getAvailable(), + lastBooking, + nextBooking, + comments ); } } 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 9499466..bf8fe78 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,23 @@ package ru.practicum.shareit.item.model; -import lombok.AllArgsConstructor; -import lombok.Data; -import ru.practicum.shareit.request.ItemRequest; +import jakarta.persistence.*; +import lombok.*; import ru.practicum.shareit.user.model.User; +@NoArgsConstructor @AllArgsConstructor -@Data +@Getter +@Setter +@Entity +@Table(name = "items") public class Item { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private String description; private Boolean available; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "owner_id") private User owner; - private ItemRequest request; } diff --git a/src/main/java/ru/practicum/shareit/item/repository/ItemRepository.java b/src/main/java/ru/practicum/shareit/item/repository/ItemRepository.java new file mode 100644 index 0000000..b71ebeb --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/repository/ItemRepository.java @@ -0,0 +1,17 @@ +package ru.practicum.shareit.item.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.user.model.User; + +import java.util.List; + +public interface ItemRepository extends JpaRepository { + List findAllByOwner(User user); + + @Query("SELECT i FROM Item i " + + "WHERE UPPER(i.name) LIKE UPPER(CONCAT('%', ?1, '%')) " + + " OR UPPER(i.description) LIKE UPPER(CONCAT('%', ?1, '%'))") + List search(String text); +} diff --git a/src/main/java/ru/practicum/shareit/item/service/ItemService.java b/src/main/java/ru/practicum/shareit/item/service/ItemService.java index bcc43e4..9a23864 100644 --- a/src/main/java/ru/practicum/shareit/item/service/ItemService.java +++ b/src/main/java/ru/practicum/shareit/item/service/ItemService.java @@ -1,5 +1,8 @@ package ru.practicum.shareit.item.service; +import ru.practicum.shareit.item.comment.dto.CommentRequestDto; +import ru.practicum.shareit.item.comment.dto.CommentResponseDto; +import ru.practicum.shareit.item.dto.ItemBookingDto; import ru.practicum.shareit.item.dto.ItemDto; import java.util.List; @@ -9,9 +12,11 @@ public interface ItemService { ItemDto updateItem(Long itemId, Long userId, ItemDto itemDto); - ItemDto findItemById(Long itemId); + ItemBookingDto findItemById(Long userId, Long itemId); - List findUserItems(Long userId); + List findUserItems(Long userId); List searchItem(String text); + + CommentResponseDto createComment(Long userId, Long itemId, CommentRequestDto commentRequestDto); } diff --git a/src/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java b/src/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java index 2e4c3f3..33c7743 100644 --- a/src/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java +++ b/src/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java @@ -2,19 +2,27 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; +import ru.practicum.shareit.booking.dto.mapper.BookingMapper; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.repository.BookingRepository; import ru.practicum.shareit.exception.NotFoundException; import ru.practicum.shareit.exception.ValidationException; -import ru.practicum.shareit.item.dao.ItemStorage; +import ru.practicum.shareit.item.comment.dto.CommentRequestDto; +import ru.practicum.shareit.item.comment.dto.CommentResponseDto; +import ru.practicum.shareit.item.comment.dto.mapper.CommentMapper; +import ru.practicum.shareit.item.comment.repository.CommentRepository; +import ru.practicum.shareit.item.dto.ItemBookingDto; +import ru.practicum.shareit.item.repository.ItemRepository; import ru.practicum.shareit.item.dto.ItemDto; import ru.practicum.shareit.item.dto.mapper.ItemMapper; import ru.practicum.shareit.item.model.Item; import ru.practicum.shareit.user.model.User; -import ru.practicum.shareit.user.dao.UserStorage; +import ru.practicum.shareit.user.repository.UserRepository; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; +import java.time.LocalDateTime; +import java.util.*; import java.util.stream.Collectors; @Slf4j @@ -22,43 +30,110 @@ @Service public class ItemServiceImpl implements ItemService { - private final ItemStorage itemStorage; - private final UserStorage userStorage; + private final ItemRepository itemRepository; + private final UserRepository userRepository; + private final BookingRepository bookingRepository; + private final CommentRepository commentRepository; @Override public ItemDto createItem(Long userId, ItemDto itemDto) { - User user = userStorage.findUserById(userId); - validateUser(user, userId); + User user = validateUser(userId); - return ItemMapper.toItemDto(itemStorage.createItem( + return ItemMapper.toItemDto(itemRepository.save( ItemMapper.toItem(user, itemDto)) ); } @Override public ItemDto updateItem(Long itemId, Long userId, ItemDto itemDto) { - User user = userStorage.findUserById(userId); - validateUser(user, userId); + validateUser(userId); + Item item = validateItem(itemId); - if (!Objects.equals(userId, itemStorage.findItemById(itemId).getOwner().getId())) { + if (!Objects.equals(userId, item.getOwner().getId())) { log.warn("Внесение изменений доступно владельцу"); throw new ValidationException("Внесение изменений доступно владельцу"); } - Item item = ItemMapper.toItem(itemId, user, itemDto); - return ItemMapper.toItemDto(itemStorage.updateItem(userId, item)); + if (itemDto.getName() != null && !itemDto.getName().isBlank()) { + item.setName(itemDto.getName()); + } + if (itemDto.getDescription() != null && !itemDto.getDescription().isBlank()) { + item.setDescription(itemDto.getDescription()); + } + if (itemDto.getAvailable() != null) { + item.setAvailable(itemDto.getAvailable()); + } + + return ItemMapper.toItemDto(itemRepository.save(item)); } @Override - public ItemDto findItemById(Long itemId) { - return ItemMapper.toItemDto(itemStorage.findItemById(itemId)); + public ItemBookingDto findItemById(Long userId, Long itemId) { + Item item = validateItem(itemId); + validateUser(userId); + + List comments = commentRepository.findByItemIdIn(List.of(itemId)).stream() + .map(CommentMapper::toCommentResponseDto) + .collect(Collectors.toList()); + + if (!Objects.equals(item.getOwner().getId(), userId)) { + return ItemMapper.toItemBookingDto(item, null, null, comments); + } + + Sort sortDescByEnd = Sort.by(Sort.Direction.DESC, "end"); + Sort sortAscByStart = Sort.by(Sort.Direction.ASC, "start"); + + Booking lastBooking = bookingRepository.findLastBookingsForItems(List.of(itemId), sortDescByEnd).stream() + .findFirst() + .orElse(null); + + Booking nextBooking = bookingRepository.findNextBookingsForItems(List.of(itemId), sortAscByStart).stream() + .findFirst() + .orElse(null); + + return ItemMapper.toItemBookingDto( + item, + BookingMapper.toSimplifiedBookingDto(lastBooking), + BookingMapper.toSimplifiedBookingDto(nextBooking), + comments + ); } @Override - public List findUserItems(Long userId) { - return itemStorage.findUserItems().stream() - .filter(item -> Objects.equals(item.getOwner().getId(), userId)) - .map(ItemMapper::toItemDto) + public List findUserItems(Long userId) { + User owner = validateUser(userId); + List items = itemRepository.findAllByOwner(owner); + + List itemIds = items.stream() + .map(Item::getId) + .collect(Collectors.toList()); + + Sort sortDescByEnd = Sort.by(Sort.Direction.DESC, "end"); + Sort sortAscByStart = Sort.by(Sort.Direction.ASC, "start"); + + Map lastBookingsMap = bookingRepository.findLastBookingsForItems(itemIds, sortDescByEnd).stream() + .collect(Collectors.toMap(lb -> lb.getItem().getId(), lb -> lb)); + + Map nextBookingsMap = bookingRepository.findNextBookingsForItems(itemIds, sortAscByStart).stream() + .collect(Collectors.toMap(nb -> nb.getItem().getId(), nb -> nb)); + + Map> commentsMap = commentRepository.findByItemIdIn(itemIds).stream() + .map(CommentMapper::toCommentResponseDto) + .collect(Collectors.groupingBy(CommentResponseDto::getItemId)); + + return items.stream() + .map(item -> { + Booking lastBooking = lastBookingsMap.get(item.getId()); + Booking nextBooking = nextBookingsMap.get(item.getId()); + List comments = commentsMap.getOrDefault(item.getId(), Collections.emptyList()); + + return ItemMapper.toItemBookingDto( + item, + BookingMapper.toSimplifiedBookingDto(lastBooking), + BookingMapper.toSimplifiedBookingDto(nextBooking), + comments + ); + }) .collect(Collectors.toList()); } @@ -68,21 +143,35 @@ public List searchItem(String text) { return new ArrayList<>(); } - return itemStorage.searchItem().stream() - .filter(Objects::nonNull) - .filter(item -> - (item.getName() != null && item.getName().toLowerCase().contains(text.toLowerCase())) || - (item.getDescription() != null && item.getDescription().toLowerCase().contains(text.toLowerCase())) - ) - .filter(item -> item.getAvailable() != null && item.getAvailable()) + return itemRepository.search(text).stream() + .filter(item -> Objects.equals(item.getAvailable(), Boolean.TRUE)) .map(ItemMapper::toItemDto) .collect(Collectors.toList()); } - private void validateUser(User user, Long userId) { - if (user == null) { - log.warn("Пользователь с данным id {} не найден", userId); - throw new NotFoundException("Пользователь с данным id: " + userId + " не найден"); + @Override + public CommentResponseDto createComment(Long userId, Long itemId, CommentRequestDto commentRequestDto) { + User author = validateUser(userId); + Item item = validateItem(itemId); + Sort sortDescByEnd = Sort.by(Sort.Direction.DESC, "end"); + + Booking booking = bookingRepository.findFirstByBookerIdAndItemIdAndEndIsBefore(userId, itemId, LocalDateTime.now(), sortDescByEnd); + + if (booking == null) { + log.warn("Пользователь может оставить комментарий после аренды"); + throw new ValidationException("Пользователь может оставить комментарий после аренды"); } + + return CommentMapper.toCommentResponseDto(commentRepository.save(CommentMapper.toComment(commentRequestDto, author, item))); + } + + private User validateUser(Long userId) { + return userRepository.findById(userId) + .orElseThrow(() -> new NotFoundException("Пользователь с данным id: " + userId + " не найден")); + } + + private Item validateItem(Long itemId) { + return itemRepository.findById(itemId) + .orElseThrow(() -> new NotFoundException("Элемент с данным id: " + itemId + " не найден")); } } diff --git a/src/main/java/ru/practicum/shareit/request/ItemRequest.java b/src/main/java/ru/practicum/shareit/request/ItemRequest.java index 13bdc7f..89006f7 100644 --- a/src/main/java/ru/practicum/shareit/request/ItemRequest.java +++ b/src/main/java/ru/practicum/shareit/request/ItemRequest.java @@ -1,7 +1,8 @@ package ru.practicum.shareit.request; import lombok.AllArgsConstructor; -import lombok.Data; +import lombok.Getter; +import lombok.Setter; import ru.practicum.shareit.user.model.User; import java.time.LocalDateTime; @@ -10,7 +11,8 @@ * TODO Sprint add-item-requests. */ @AllArgsConstructor -@Data +@Getter +@Setter public class ItemRequest { private Long id; private String description; diff --git a/src/main/java/ru/practicum/shareit/user/dao/UserStorage.java b/src/main/java/ru/practicum/shareit/user/dao/UserStorage.java deleted file mode 100644 index 9c43da2..0000000 --- a/src/main/java/ru/practicum/shareit/user/dao/UserStorage.java +++ /dev/null @@ -1,18 +0,0 @@ -package ru.practicum.shareit.user.dao; - -import ru.practicum.shareit.user.model.User; - -import java.util.List; - -public interface UserStorage { - - List findAllUsers(); - - User findUserById(Long id); - - User createUser(User user); - - User updateUser(User updateUser); - - void deleteUser(Long id); -} diff --git a/src/main/java/ru/practicum/shareit/user/dao/UserStorageImpl.java b/src/main/java/ru/practicum/shareit/user/dao/UserStorageImpl.java deleted file mode 100644 index 8c764f6..0000000 --- a/src/main/java/ru/practicum/shareit/user/dao/UserStorageImpl.java +++ /dev/null @@ -1,63 +0,0 @@ -package ru.practicum.shareit.user.dao; - -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Repository; -import ru.practicum.shareit.exception.NotFoundException; -import ru.practicum.shareit.user.model.User; - -import java.util.*; - -@Slf4j -@Repository -public class UserStorageImpl implements UserStorage { - private Map users = new HashMap<>(); - - @Override - public List findAllUsers() { - return new ArrayList<>(users.values()); - } - - @Override - public User findUserById(Long id) { - return users.get(id); - } - - @Override - public User createUser(User user) { - user.setId(getId()); - users.put(user.getId(), user); - return user; - } - - @Override - public User updateUser(User updateUser) { - if (!users.containsKey(updateUser.getId())) { - log.warn("Обновляемый пользователь не найден"); - throw new NotFoundException("Обновляемый пользователь не найден"); - } else { - User oldUser = users.get(updateUser.getId()); - if (updateUser.getName() == null) { - updateUser.setName(oldUser.getName()); - } - if (updateUser.getEmail() == null) { - updateUser.setEmail(oldUser.getEmail()); - } - } - - users.put(updateUser.getId(), updateUser); - return updateUser; - } - - @Override - public void deleteUser(Long id) { - users.remove(id); - } - - private Long getId() { - long lastId = users.keySet().stream() - .mapToLong(Long::longValue) - .max() - .orElse(0); - return lastId + 1; - } -} 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 50d7144..bf78cf7 100644 --- a/src/main/java/ru/practicum/shareit/user/dto/UserDto.java +++ b/src/main/java/ru/practicum/shareit/user/dto/UserDto.java @@ -2,13 +2,12 @@ import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; +import lombok.*; @NoArgsConstructor @AllArgsConstructor -@Data +@Getter +@Setter public class UserDto { private Long id; @NotBlank(message = "Имя не может быть null") diff --git a/src/main/java/ru/practicum/shareit/user/dto/mapper/UserMapper.java b/src/main/java/ru/practicum/shareit/user/dto/mapper/UserMapper.java index bf99f95..e357d95 100644 --- a/src/main/java/ru/practicum/shareit/user/dto/mapper/UserMapper.java +++ b/src/main/java/ru/practicum/shareit/user/dto/mapper/UserMapper.java @@ -21,12 +21,4 @@ public static User toUser(UserDto userDto) { userDto.getEmail() ); } - - public static User toUser(Long id, UserDto userDto) { - return new User( - id, - userDto.getName(), - userDto.getEmail() - ); - } } 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 cdf04eb..2751309 100644 --- a/src/main/java/ru/practicum/shareit/user/model/User.java +++ b/src/main/java/ru/practicum/shareit/user/model/User.java @@ -1,11 +1,17 @@ package ru.practicum.shareit.user.model; -import lombok.AllArgsConstructor; -import lombok.Data; +import jakarta.persistence.*; +import lombok.*; +@NoArgsConstructor @AllArgsConstructor -@Data +@Getter +@Setter +@Entity +@Table(name = "users") public class User { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private String email; diff --git a/src/main/java/ru/practicum/shareit/user/repository/UserRepository.java b/src/main/java/ru/practicum/shareit/user/repository/UserRepository.java new file mode 100644 index 0000000..c79d1e1 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/user/repository/UserRepository.java @@ -0,0 +1,10 @@ +package ru.practicum.shareit.user.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import ru.practicum.shareit.user.model.User; + +import java.util.Optional; + +public interface UserRepository extends JpaRepository { + Optional findByEmail(String email); +} diff --git a/src/main/java/ru/practicum/shareit/user/service/UserServiceImpl.java b/src/main/java/ru/practicum/shareit/user/service/UserServiceImpl.java index 905c72c..e5d966b 100644 --- a/src/main/java/ru/practicum/shareit/user/service/UserServiceImpl.java +++ b/src/main/java/ru/practicum/shareit/user/service/UserServiceImpl.java @@ -8,10 +8,9 @@ import ru.practicum.shareit.user.dto.UserDto; import ru.practicum.shareit.user.dto.mapper.UserMapper; import ru.practicum.shareit.user.model.User; -import ru.practicum.shareit.user.dao.UserStorage; +import ru.practicum.shareit.user.repository.UserRepository; import java.util.List; -import java.util.Objects; import java.util.stream.Collectors; @Slf4j @@ -19,23 +18,19 @@ @Service public class UserServiceImpl implements UserService { - private final UserStorage userStorage; + private final UserRepository userRepository; @Override public List findAllUsers() { - return userStorage.findAllUsers().stream() + return userRepository.findAll().stream() .map(UserMapper::toUserDto) .collect(Collectors.toList()); } @Override public UserDto findUserById(Long id) { - User user = userStorage.findUserById(id); + User user = validateUser(id); - if (user == null) { - log.warn("Пользователь с данным id {} не найден", id); - throw new NotFoundException("Пользователь с данным id: " + id + " не найден"); - } return UserMapper.toUserDto(user); } @@ -43,34 +38,43 @@ public UserDto findUserById(Long id) { public UserDto createUser(UserDto userDto) { validateEmail(null, userDto); - return UserMapper.toUserDto(userStorage.createUser(UserMapper.toUser(userDto))); + return UserMapper.toUserDto(userRepository.save(UserMapper.toUser(userDto))); } @Override public UserDto updateUser(Long id, UserDto updateUserDto) { - User existingUser = userStorage.findUserById(id); + User existingUser = validateUser(id); + + validateEmail(id, updateUserDto); - if (existingUser == null) { - log.warn("Пользователь с id: {} не найден", id); - throw new NotFoundException("Пользователь с данным id " + id + " не найден."); + if (updateUserDto.getName() != null && !updateUserDto.getName().isBlank()) { + existingUser.setName(updateUserDto.getName()); } - validateEmail(id, updateUserDto); + if (updateUserDto.getEmail() != null && !updateUserDto.getEmail().isBlank()) { + existingUser.setEmail(updateUserDto.getEmail()); + } - return UserMapper.toUserDto(userStorage.updateUser(UserMapper.toUser(id, updateUserDto))); + return UserMapper.toUserDto(userRepository.save(existingUser)); } @Override public void deleteUser(Long id) { - userStorage.deleteUser(id); + userRepository.deleteById(id); } private void validateEmail(Long id, UserDto userDto) { - if (userStorage.findAllUsers().stream() - .anyMatch(viewedUser -> Objects.equals(viewedUser.getEmail(), userDto.getEmail()) - && !Objects.equals(viewedUser.getId(), id))) { - log.warn("Данная электронная почта {} уже существует", userDto.getEmail()); - throw new InternalServerException("Данная электронная почта уже существует"); - } + userRepository.findByEmail(userDto.getEmail()) + .ifPresent(user -> { + if (!user.getId().equals(id)) { + log.warn("Данная электронная почта уже существует"); + throw new InternalServerException("Данная электронная почта уже существует"); + } + }); + } + + private User validateUser(Long userId) { + return userRepository.findById(userId) + .orElseThrow(() -> new NotFoundException("Пользователь с данным id: " + userId + " не найден")); } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index b9e5d4b..0000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1,14 +0,0 @@ -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 DB -#spring.datasource.driverClassName -#spring.datasource.url -#spring.datasource.username -#spring.datasource.password diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml new file mode 100644 index 0000000..f5599d8 --- /dev/null +++ b/src/main/resources/application.yaml @@ -0,0 +1,35 @@ +server: + port: 8080 + +spring: + sql: + init: + mode: always + jpa: + show-sql: true + hibernate: + ddl-auto: none + properties: + hibernate: + format_sql: true + jdbc: + time_zone: UTC + lob: + non_contextual_creation: true + + datasource: + url: jdbc:postgresql://localhost:5438/shareitdb + username: dbuser + password: 12345 + driver-class-name: org.postgresql.Driver + +logging: + level: + org: + springframework: + orm: + jpa: INFO + jpa.JpaTransactionManager: DEBUG + transaction: + interceptor: TRACE + transaction.interceptor: TRACE \ 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..fa318ff --- /dev/null +++ b/src/main/resources/schema.sql @@ -0,0 +1,55 @@ +drop table IF EXISTS users CASCADE; +drop table IF EXISTS items CASCADE; +drop table IF EXISTS bookings CASCADE; +drop table IF EXISTS requests CASCADE; +drop table IF EXISTS comments CASCADE; + +create TABLE IF NOT EXISTS users ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + name VARCHAR(255) NOT NULL, + email VARCHAR(512) NOT NULL, + CONSTRAINT pk_user PRIMARY KEY (id), + CONSTRAINT UQ_USER_EMAIL UNIQUE (email) +); + +create TABLE IF NOT EXISTS items ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + name VARCHAR(255) NOT NULL, + description VARCHAR(2000), + available BOOLEAN DEFAULT TRUE, + owner_id BIGINT NOT NULL, + CONSTRAINT pk_item PRIMARY KEY (id), + CONSTRAINT fk_item_owner FOREIGN KEY (owner_id) REFERENCES users(id) +); + +create TABLE IF NOT EXISTS bookings ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + 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(50), + CONSTRAINT pk_booking PRIMARY KEY (id), + CONSTRAINT fk_booking_item FOREIGN KEY (item_id) REFERENCES items(id), + CONSTRAINT fk_booking_booker FOREIGN KEY (booker_id) REFERENCES users(id) +); + +create TABLE IF NOT EXISTS requests ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + description VARCHAR(2000), + requestor_id BIGINT NOT NULL, + CONSTRAINT pk_request PRIMARY KEY (id), + CONSTRAINT fk_request_requestor FOREIGN KEY (requestor_id) REFERENCES users(id) +); + +create TABLE IF NOT EXISTS comments ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + text VARCHAR(2000), + item_id BIGINT NOT NULL, + author_id BIGINT NOT NULL, + created TIMESTAMP WITHOUT TIME ZONE NOT NULL, + CONSTRAINT pk_comment PRIMARY KEY (id), + CONSTRAINT fk_comment_item FOREIGN KEY (item_id) REFERENCES items(id), + CONSTRAINT fk_comment_author FOREIGN KEY (author_id) REFERENCES users(id) +); + diff --git a/src/test/java/ru/practicum/shareit/ShareItTests.java b/src/test/java/ru/practicum/shareit/ShareItTests.java index 4d79052..6dd7261 100644 --- a/src/test/java/ru/practicum/shareit/ShareItTests.java +++ b/src/test/java/ru/practicum/shareit/ShareItTests.java @@ -1,3 +1,4 @@ +/* package ru.practicum.shareit; import org.junit.jupiter.api.Test; @@ -11,3 +12,4 @@ void contextLoads() { } } +*/