diff --git a/README.md b/README.md index 47a75f0..9fbff3f 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,4 @@ # java-shareit -Template repository for Shareit project. + +### Database map +![SchemaDatabase.png](SchemaDatabase.png) diff --git a/SchemaDatabase.png b/SchemaDatabase.png new file mode 100644 index 0000000..1f547e7 Binary files /dev/null and b/SchemaDatabase.png differ diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..ae1e654 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,16 @@ +services: + db: + image: postgres:16.1 + ports: + - "5432:5432" + volumes: + - ./volumes/postgres:/var/lib/postgresql/data/ + environment: + - POSTGRES_DB=shareit + - POSTGRES_USER=dbuser + - POSTGRES_PASSWORD=12345 + healthcheck: + test: pg_isready -q -d $$POSTGRES_DB -U $$POSTGRES_USER + timeout: 5s + interval: 5s + retries: 10 diff --git a/lombok.config b/lombok.config index b0056c1..fa7bee0 100644 --- a/lombok.config +++ b/lombok.config @@ -1,4 +1,10 @@ config.stopBubbling = true lombok.anyconstructor.addconstructorproperties = false lombok.addLombokGeneratedAnnotation = true -lombok.addSuppressWarnings = false \ No newline at end of file +lombok.addSuppressWarnings = false +lombok.fieldDefaults.defaultPrivate = true +lombok.equalsAndHashCode.callSuper = call +lombok.equalsAndHashCode.doNotUseGetters = true +lombok.toString.callSuper = call +lombok.toString.doNotUseGetters = true +lombok.noArgsConstructor.extraPrivate = true diff --git a/pom.xml b/pom.xml index 1aef2f7..e05de59 100644 --- a/pom.xml +++ b/pom.xml @@ -20,6 +20,11 @@ + + org.projectlombok + lombok + true + org.springframework.boot spring-boot-starter-web @@ -33,34 +38,29 @@ spring-boot-configuration-processor true - - org.postgresql - postgresql - runtime + org.springframework.boot + spring-boot-starter-test + test - - org.projectlombok - lombok - true + org.springframework.boot + spring-boot-starter-validation - - com.h2database - h2 - test + org.springframework.boot + spring-boot-starter-data-jpa - org.springframework.boot - spring-boot-starter-test - test + org.postgresql + postgresql + runtime - org.springframework.boot - spring-boot-starter-validation + com.h2database + h2 + test - org.zalando logbook-spring-boot-starter 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 4b63363..0000000 --- a/src/main/java/ru/practicum/shareit/booking/Booking.java +++ /dev/null @@ -1,23 +0,0 @@ -package ru.practicum.shareit.booking; - -import lombok.AccessLevel; -import lombok.Data; -import lombok.experimental.FieldDefaults; -import ru.practicum.shareit.item.model.Item; -import ru.practicum.shareit.user.User; - -import java.time.LocalDate; - -/** - * TODO Sprint add-bookings. - */ -@Data -@FieldDefaults(level = AccessLevel.PRIVATE) -public class Booking { - long id; - LocalDate start; - LocalDate end; - Item item; - User booker; - BookingStatus status; -} diff --git a/src/main/java/ru/practicum/shareit/booking/BookingController.java b/src/main/java/ru/practicum/shareit/booking/BookingController.java index b94493d..84bbaa7 100644 --- a/src/main/java/ru/practicum/shareit/booking/BookingController.java +++ b/src/main/java/ru/practicum/shareit/booking/BookingController.java @@ -1,12 +1,65 @@ package ru.practicum.shareit.booking; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import ru.practicum.shareit.booking.dto.BookingDto; +import ru.practicum.shareit.booking.dto.IncomingBookingDto; +import ru.practicum.shareit.booking.service.BookingService; +import ru.practicum.shareit.util.Marker; + +import java.util.List; /** * TODO Sprint add-bookings. */ @RestController +@Validated @RequestMapping(path = "/bookings") +@RequiredArgsConstructor public class BookingController { + final BookingService bookingService; + + @GetMapping("/{bookingId}") + public BookingDto getBooking( + @RequestHeader("X-Sharer-User-Id") Long userId, + @PathVariable long bookingId + ) { + return bookingService.getBooking(userId, bookingId); + } + + @GetMapping + public List getAllBookingsByBooker( + @RequestHeader("X-Sharer-User-Id") long userId, + @RequestParam(required = false) String state + ) { + return bookingService.getAllBookingsByBooker(userId, state); + } + + @GetMapping("/owner") + public List getAllBookingsByOwner( + @RequestHeader("X-Sharer-User-Id") long userId, + @RequestParam(required = false) String state + ) { + return bookingService.getAllBookingsByOwner(userId, state); + } + + @PostMapping + @Validated(Marker.OnCreate.class) + public BookingDto addNewBooking( + @RequestHeader("X-Sharer-User-Id") Long userId, + @Valid @RequestBody IncomingBookingDto bookingDto + ) { + return bookingService.addNewBooking(userId, bookingDto); + } + + @PatchMapping("/{bookingId}") + public BookingDto approveBooking( + @RequestHeader("X-Sharer-User-Id") long userId, + @PathVariable long bookingId, + @RequestParam boolean approved + ) { + return bookingService.approveBooking(userId, bookingId, approved); + } } diff --git a/src/main/java/ru/practicum/shareit/booking/BookingStatus.java b/src/main/java/ru/practicum/shareit/booking/BookingStatus.java deleted file mode 100644 index 8fd4d81..0000000 --- a/src/main/java/ru/practicum/shareit/booking/BookingStatus.java +++ /dev/null @@ -1,8 +0,0 @@ -package ru.practicum.shareit.booking; - -public enum BookingStatus { - WAITING, // новое бронирование, ожидает одобрения - APPROVED, // бронирование подтверждено владельцем - REJECTED, // бронирование отклонено владельцем - CANCELED // бронирование отменено создателем -} 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..fb00380 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,21 @@ package ru.practicum.shareit.booking.dto; +import jakarta.validation.constraints.NotNull; +import ru.practicum.shareit.booking.model.BookingStatus; +import ru.practicum.shareit.item.dto.ItemDto; +import ru.practicum.shareit.user.dto.UserDto; + +import java.time.LocalDateTime; + /** * TODO Sprint add-bookings. */ -public class BookingDto { -} +@NotNull +public record BookingDto( + Long id, + LocalDateTime start, + LocalDateTime end, + ItemDto item, + UserDto booker, + BookingStatus status +) {} diff --git a/src/main/java/ru/practicum/shareit/booking/dto/BookingMapper.java b/src/main/java/ru/practicum/shareit/booking/dto/BookingMapper.java new file mode 100644 index 0000000..9520b4a --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/dto/BookingMapper.java @@ -0,0 +1,36 @@ +package ru.practicum.shareit.booking.dto; + +import org.springframework.lang.NonNull; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.model.BookingStatus; +import ru.practicum.shareit.item.dto.ItemMapper; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.user.dto.UserMapper; +import ru.practicum.shareit.user.model.User; + +import java.time.ZoneId; + +public class BookingMapper { + + @NonNull + public static BookingDto toBookingDto(@NonNull Booking booking) { + return new BookingDto( + booking.getId(), + booking.getStart().atZone(ZoneId.systemDefault()).toLocalDateTime(), + booking.getEnd().atZone(ZoneId.systemDefault()).toLocalDateTime(), + ItemMapper.toItemDto(booking.getItem()), + UserMapper.toUserDto(booking.getBooker()), + booking.getStatus() + ); + } + + public static Booking fromIncomingDto(@NonNull IncomingBookingDto bookingDto, Item item, User booker) { + return new Booking() + .setId(null) + .setStart(bookingDto.getStart()) + .setEnd(bookingDto.getEnd()) + .setBooker(booker) + .setItem(item) + .setStatus(BookingStatus.WAITING); + } +} diff --git a/src/main/java/ru/practicum/shareit/booking/dto/IncomingBookingDto.java b/src/main/java/ru/practicum/shareit/booking/dto/IncomingBookingDto.java new file mode 100644 index 0000000..4c95ab3 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/dto/IncomingBookingDto.java @@ -0,0 +1,23 @@ +package ru.practicum.shareit.booking.dto; + +import jakarta.validation.constraints.*; +import lombok.Data; +import ru.practicum.shareit.util.Marker; + +import java.time.LocalDateTime; + +@Data +@NotNull +public class IncomingBookingDto { + + @Positive(message = "id вещи - обязательный параметр, должен быть положительным числом") + long itemId; + + @FutureOrPresent(message = "Дата начала не может быть в прошлом") + @NotNull(groups = Marker.OnCreate.class, message = "Дата начала бронирования - обязательна") + LocalDateTime start; + + @Future(message = "Дата окончания должна быть в будущем") + @NotNull(groups = Marker.OnCreate.class, message = "Дата окончания бронирования - обязательна") + LocalDateTime end; +} 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..b0d322f --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/model/Booking.java @@ -0,0 +1,64 @@ +package ru.practicum.shareit.booking.model; + +import jakarta.persistence.*; +import lombok.*; +import lombok.experimental.Accessors; +import lombok.experimental.ExtensionMethod; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.user.model.User; +import ru.practicum.shareit.util.hibernate.HibernateEqualsAndHashCode; + +import java.time.LocalDateTime; +import java.util.Objects; + +import static ru.practicum.shareit.util.hibernate.HibernateEqualsAndHashCode.*; + +/** + * TODO Sprint add-bookings. + */ + +@Data +@Entity +@NoArgsConstructor +@Accessors(chain = true) +@Table(name = "bookings") +@ExtensionMethod({HibernateEqualsAndHashCode.class}) +public class Booking { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + Long id; + + @Column(name = "start_date", nullable = false) + LocalDateTime start; + + @Column(name = "end_date", nullable = false) + LocalDateTime end; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "item_id", nullable = false) + Item item; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "booker_id", nullable = false) + User booker; + + @Enumerated(EnumType.STRING) + BookingStatus status; + + //region equals and hashCode + @Override + public final boolean equals(Object object) { + return this == object + || object != null + && persistentClass(this) == object.persistentClass() + && object instanceof Booking booking + && Objects.equals(getId(), booking.getId()); + } + + @Override + public final int hashCode() { + return persistentClass(this).hashCode(); + } + //endregion +} 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..a5b6420 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/model/BookingStatus.java @@ -0,0 +1,17 @@ +package ru.practicum.shareit.booking.model; + +/// Статус бронирования вещи +public enum BookingStatus { + + /// новое бронирование, ожидает одобрения + WAITING, + + /// бронирование подтверждено владельцем + APPROVED, + + /// бронирование отклонено владельцем + REJECTED, + + /// бронирование отменено создателем + CANCELED +} diff --git a/src/main/java/ru/practicum/shareit/booking/model/StateOfBooking.java b/src/main/java/ru/practicum/shareit/booking/model/StateOfBooking.java new file mode 100644 index 0000000..0190228 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/model/StateOfBooking.java @@ -0,0 +1,39 @@ +package ru.practicum.shareit.booking.model; + +import lombok.Getter; +import org.springframework.lang.NonNull; + +import java.util.Arrays; +import java.util.List; + +/// Параметр запроса списка бронирований +public enum StateOfBooking { + UNDEFINED, ALL, CURRENT, PAST, FUTURE, WAITING, REJECTED; + + @Getter + private String invalidValue; + + public static StateOfBooking of(String text) { + if (text == null) { + return ALL; + } + String state = Arrays.stream(StateOfBooking.values()) + .map(StateOfBooking::name) + .filter(value -> text.toUpperCase().equals(value)) + .findAny().orElse(null); + + if (state == null) { + StateOfBooking so = UNDEFINED; + so.invalidValue = text; + return so; + } + return StateOfBooking.valueOf(state); + } + + @NonNull + public static List getValidValues() { + return Arrays.stream(values()).filter(v -> v.ordinal() > 0) + .map(StateOfBooking::name) + .toList(); + } +} 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..08fb87b --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java @@ -0,0 +1,115 @@ +package ru.practicum.shareit.booking.repository; + +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.time.LocalDateTime; +import java.util.List; +import java.util.Set; + +public interface BookingRepository extends JpaRepository { + + @Query(""" + select b + from Booking b + left join fetch Item i on b.item.id = i.id + where i.owner.id = :ownerId + order by b.start desc + """) + List findAllByOwner(Long ownerId); + + @Query(""" + select b + from Booking b + left join fetch Item i on i.id = b.item.id + where i.owner.id = :ownerId + and b.start <= CURRENT_TIMESTAMP + and b.end >= CURRENT_TIMESTAMP + order by b.start desc + """) + List findCurrentByOwner(Long ownerId); + + @Query(""" + select b + from Booking b + left join fetch Item i on i.id = b.item.id + where i.owner.id = :ownerId + and b.end < CURRENT_TIMESTAMP + order by b.start desc + """) + List findPastByOwner(Long ownerId); + + @Query(""" + select b + from Booking b + left join fetch Item i on i.id = b.item.id + where i.owner.id = :ownerId + and b.start > CURRENT_TIMESTAMP + order by b.start desc + """) + List findFutureByOwner(Long ownerId); + + @Query(""" + select b + from Booking b + left join fetch Item i on i.id = b.item.id + where i.owner.id = :ownerId + and b.status = :status + order by b.start desc + """) + List findAllByOwnerAndStatusOrderByStartDesc(Long ownerId, BookingStatus status); + + @Query(""" + select b + from Booking b + where b.booker.id = :bookerId + and b.start <= CURRENT_TIMESTAMP + and b.end >= CURRENT_TIMESTAMP + order by b.start desc + """) + List findCurrentByBooker(Long bookerId); + + @Query(""" + select b + from Booking b + where b.booker.id = :bookerId + and b.end < CURRENT_TIMESTAMP + order by b.start desc + """) + List findPastByBooker(Long bookerId); + + @Query(""" + select b + from Booking b + where b.booker.id = :bookerId + and b.start > CURRENT_TIMESTAMP + order by b.start desc + """) + List findFutureByBooker(Long bookerId); + + @Query(""" + SELECT MAX(b.start) + FROM Booking b + WHERE b.start <= CURRENT_TIMESTAMP + AND b.item.id = :itemId + """) + LocalDateTime findLastBookingDate(Long itemId); + + @Query(""" + SELECT MIN(b.start) + FROM Booking b + WHERE b.start > CURRENT_TIMESTAMP + AND b.item.id = :itemId + """) + LocalDateTime findNextBookingDate(Long itemId); + + List findAllByBookerIdOrderByStartDesc(Long bookerId); + + List findAllByBooker_IdAndStatusOrderByStartDesc(Long bookerId, BookingStatus status); + + List findAllBookingByItem_Id(Long itemId); + + List findAllBookingByItemIdIn(Set itemIds); +} 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..3abc913 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/service/BookingService.java @@ -0,0 +1,29 @@ +package ru.practicum.shareit.booking.service; + +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.shareit.booking.dto.BookingDto; +import ru.practicum.shareit.booking.dto.IncomingBookingDto; + +import java.util.List; + +@Transactional(readOnly = true) +public interface BookingService { + + /// Получение данных о конкретном бронировании + BookingDto getBooking(Long userId, long bookingId); + + /// Получение списка всех бронирований текущего пользователя + List getAllBookingsByBooker(long userId, String state); + + /// Получение списка бронирований для всех вещей текущего пользователя + List getAllBookingsByOwner(long userId, String state); + + /// Добавление нового бронирования + @Transactional + BookingDto addNewBooking(Long userId, IncomingBookingDto bookingDto); + + /// Согласование бронирования + @Transactional(propagation = Propagation.REQUIRED) + BookingDto approveBooking(long userId, long bookingId, boolean approved); +} 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..2cbfcbf --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/service/BookingServiceImpl.java @@ -0,0 +1,215 @@ +package ru.practicum.shareit.booking.service; + +import jakarta.validation.ValidationException; +import lombok.RequiredArgsConstructor; +import lombok.experimental.FieldDefaults; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Service; +import ru.practicum.shareit.booking.dto.BookingDto; +import ru.practicum.shareit.booking.dto.BookingMapper; +import ru.practicum.shareit.booking.dto.IncomingBookingDto; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.model.BookingStatus; +import ru.practicum.shareit.booking.model.StateOfBooking; +import ru.practicum.shareit.booking.repository.BookingRepository; +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 ru.practicum.shareit.util.exception.BookingStatusException; +import ru.practicum.shareit.util.exception.ForbiddenException; +import ru.practicum.shareit.util.exception.NotFoundException; +import ru.practicum.shareit.util.exception.ParameterNotValidException; + +import java.time.LocalDateTime; +import java.util.List; + +@Service +@RequiredArgsConstructor +@FieldDefaults(makeFinal = true) +public class BookingServiceImpl implements BookingService { + + BookingRepository bookingRepository; + ItemRepository itemRepository; + UserRepository userRepository; + + @Override + public BookingDto addNewBooking(Long userId, @NonNull IncomingBookingDto bookingDto) { + LocalDateTime start = bookingDto.getStart(); + LocalDateTime end = bookingDto.getEnd(); + if (start.isAfter(end) || start.isEqual(end)) { + throw new ValidationException("Дата начала бронирования должна быть раньше даты окончания"); + } + + User booker = userRepository.findById(userId).orElseThrow( + () -> new ForbiddenException("Пользователь с id=" + userId + " не найден") + ); + + long itemId = bookingDto.getItemId(); + Item item = itemRepository.findById(itemId).orElseThrow( + () -> new NotFoundException("Вещь с id=" + itemId + " не найдена") + ); + if (item.getOwner().getId().equals(userId)) { + throw new ValidationException("Нельзя забронировать свою-же вещь"); + } + if (!item.isAvailable()) { + throw new ValidationException("Вещь с id=" + item.getId() + " не доступна для аренды"); + } + + return BookingMapper.toBookingDto( + bookingRepository.save(BookingMapper.fromIncomingDto(bookingDto, item, booker)) + ); + } + + @Override + public BookingDto approveBooking(long userId, long bookingId, boolean approved) { + checkUser(userId); + Booking booking = getBooking(bookingId); + if (userId != booking.getItem().getOwner().getId()) { + throw new ForbiddenException("Только владелец может менять статус вещи"); + } + BookingStatus status = booking.getStatus(); + switch (status) { + case WAITING -> { + if (approved) { + booking.setStatus(BookingStatus.APPROVED); + } else { + booking.setStatus(BookingStatus.REJECTED); + } + } + case REJECTED -> { + if (approved) { + booking.setStatus(BookingStatus.APPROVED); + } + } + case CANCELED -> throw new BookingStatusException("Бронирование уже было отменено создателем"); + } + + return BookingMapper.toBookingDto(booking); + } + + @Override + public BookingDto getBooking(Long userId, long bookingId) { + checkUser(userId); + Booking booking = getBooking(bookingId); + if (!booking.getBooker().getId().equals(userId) && !booking.getItem().getOwner().getId().equals(userId)) { + throw new ForbiddenException("Может быть выполнено либо автором бронирования, либо владельцем вещи"); + } + return BookingMapper.toBookingDto(booking); + } + + @Override + public List getAllBookingsByBooker(long userId, String state) { + checkUser(userId); + StateOfBooking stateOfBooking = StateOfBooking.of(state); + switch (stateOfBooking) { + case ALL -> { + return bookingRepository.findAllByBookerIdOrderByStartDesc(userId).stream() + .map(BookingMapper::toBookingDto) + .toList(); + } + case CURRENT -> { + return bookingRepository.findCurrentByBooker(userId).stream() + .map(BookingMapper::toBookingDto) + .toList(); + } + case PAST -> { + return bookingRepository.findPastByBooker(userId).stream() + .map(BookingMapper::toBookingDto) + .toList(); + } + case FUTURE -> { + return bookingRepository.findFutureByBooker(userId).stream() + .map(BookingMapper::toBookingDto) + .toList(); + } + case WAITING -> { + return bookingRepository.findAllByBooker_IdAndStatusOrderByStartDesc( + userId, + BookingStatus.WAITING + ).stream() + .map(BookingMapper::toBookingDto) + .toList(); + } + case REJECTED -> { + return bookingRepository.findAllByBooker_IdAndStatusOrderByStartDesc( + userId, + BookingStatus.REJECTED + ).stream() + .map(BookingMapper::toBookingDto) + .toList(); + } + case null, default -> { + assert stateOfBooking != null; + throw new ParameterNotValidException( + "state = " + stateOfBooking.getInvalidValue(), + "допустимые значения: " + StateOfBooking.getValidValues() + ); + } + } + } + + @Override + public List getAllBookingsByOwner(long userId, String state) { + checkUser(userId); + StateOfBooking stateOfBooking = StateOfBooking.of(state); + switch (stateOfBooking) { + case ALL -> { + return bookingRepository.findAllByOwner(userId).stream() + .map(BookingMapper::toBookingDto) + .toList(); + } + case CURRENT -> { + return bookingRepository.findCurrentByOwner(userId).stream() + .map(BookingMapper::toBookingDto) + .toList(); + } + case PAST -> { + return bookingRepository.findPastByOwner(userId).stream() + .map(BookingMapper::toBookingDto) + .toList(); + } + case FUTURE -> { + return bookingRepository.findFutureByOwner(userId).stream() + .map(BookingMapper::toBookingDto) + .toList(); + } + case WAITING -> { + return bookingRepository.findAllByOwnerAndStatusOrderByStartDesc( + userId, + BookingStatus.WAITING + ).stream() + .map(BookingMapper::toBookingDto) + .toList(); + } + case REJECTED -> { + return bookingRepository.findAllByOwnerAndStatusOrderByStartDesc( + userId, + BookingStatus.REJECTED + ).stream() + .map(BookingMapper::toBookingDto) + .toList(); + } + case null, default -> { + assert stateOfBooking != null; + throw new ParameterNotValidException( + "state = " + stateOfBooking.getInvalidValue(), + "допустимые значения: " + StateOfBooking.getValidValues() + ); + } + } + } + + private void checkUser(Long userId) { + if (!userRepository.existsById(userId)) { + throw new ForbiddenException("Пользователь с id=" + userId + " не найден."); + } + } + + @NonNull + private Booking getBooking(Long bookingId) { + return bookingRepository.findById(bookingId).orElseThrow( + () -> new NotFoundException("Запрос аренды с id=" + bookingId + " не существует") + ); + } +} diff --git a/src/main/java/ru/practicum/shareit/item/ItemController.java b/src/main/java/ru/practicum/shareit/item/ItemController.java index 360e2d7..be70df4 100644 --- a/src/main/java/ru/practicum/shareit/item/ItemController.java +++ b/src/main/java/ru/practicum/shareit/item/ItemController.java @@ -4,7 +4,9 @@ import lombok.RequiredArgsConstructor; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +import ru.practicum.shareit.item.dto.CommentDto; import ru.practicum.shareit.item.dto.ItemDto; +import ru.practicum.shareit.item.dto.ItemDtoFullVersion; import ru.practicum.shareit.item.service.ItemService; import ru.practicum.shareit.util.Marker; @@ -19,15 +21,18 @@ @RequiredArgsConstructor public class ItemController { - private final ItemService itemService; + final ItemService itemService; @GetMapping("/{itemId}") - public ItemDto getItem(@PathVariable long itemId) { - return itemService.getItem(itemId); + public ItemDtoFullVersion getItem( + @RequestHeader("X-Sharer-User-Id") long userId, + @PathVariable long itemId + ) { + return itemService.getItem(userId, itemId); } @GetMapping - public List getAllItemsOfUser(@RequestHeader("X-Sharer-User-Id") long userId) { + public List getAllItemsOfUser(@RequestHeader("X-Sharer-User-Id") long userId) { return itemService.getAllItemsOfUser(userId); } @@ -48,6 +53,15 @@ public ItemDto addNewItem( return itemService.addNewItem(userId, item); } + @PostMapping("/{itemId}/comment") + public CommentDto addComment( + @RequestHeader("X-Sharer-User-Id") long userId, + @PathVariable long itemId, + @Valid @RequestBody CommentDto commentDto + ) { + return itemService.addComment(userId, itemId, commentDto); + } + @PatchMapping("/{itemId}") public ItemDto updateItem( @RequestHeader("X-Sharer-User-Id") long userId, diff --git a/src/main/java/ru/practicum/shareit/item/dto/CommentDto.java b/src/main/java/ru/practicum/shareit/item/dto/CommentDto.java new file mode 100644 index 0000000..f52f008 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/dto/CommentDto.java @@ -0,0 +1,27 @@ +package ru.practicum.shareit.item.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Builder; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +@Builder +@NotNull +public class CommentDto { + + Long id; + + Long itemId; + + String authorName; + + @JsonProperty("created") + LocalDateTime createdDate; + + @NotBlank + String text; +} diff --git a/src/main/java/ru/practicum/shareit/item/dto/CommentMapper.java b/src/main/java/ru/practicum/shareit/item/dto/CommentMapper.java new file mode 100644 index 0000000..a6650e0 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/dto/CommentMapper.java @@ -0,0 +1,30 @@ +package ru.practicum.shareit.item.dto; + +import org.springframework.lang.NonNull; +import ru.practicum.shareit.item.model.Comment; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.user.model.User; + +import java.time.LocalDateTime; + +public class CommentMapper { + + public static CommentDto toDto(@NonNull Comment comment) { + return CommentDto.builder() + .id(comment.getId()) + .itemId(comment.getItem().getId()) + .authorName(comment.getAuthor().getName()) + .createdDate(comment.getCreatedDate()) + .text(comment.getText()) + .build(); + } + + public static Comment toComment(@NonNull CommentDto dto, @NonNull Item item, @NonNull User author) { + return new Comment() + .setId(dto.getId()) + .setItem(item) + .setAuthor(author) + .setCreatedDate(dto.getCreatedDate() == null ? LocalDateTime.now() : dto.getCreatedDate()) + .setText(dto.getText() == null ? null : dto.getText().trim()); + } +} 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 4b32e81..7db7208 100644 --- a/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java +++ b/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java @@ -2,7 +2,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import jakarta.validation.constraints.*; -import lombok.AccessLevel; +import lombok.Builder; import lombok.Data; import lombok.experimental.FieldDefaults; import ru.practicum.shareit.util.Marker; @@ -12,17 +12,18 @@ */ @Data @NotNull -@FieldDefaults(level = AccessLevel.PRIVATE) +@Builder +@FieldDefaults(makeFinal = true) public class ItemDto { Long id; @NotBlank(groups = Marker.OnCreate.class) - @Size(min = 1, max = 100) + @Size(min = 1, max = 255) String name; @NotBlank(groups = Marker.OnCreate.class) - @Size(min = 1, max = 250) + @Size(min = 1, max = 255) String description; @NotNull(groups = Marker.OnCreate.class) @@ -30,12 +31,4 @@ public class ItemDto { @JsonProperty("request") Long requestId; - - public ItemDto(Long id, String name, String description, Boolean available, Long requestId) { - this.id = id != null && id <= 0 ? null : id; - this.name = name == null ? null : name.trim(); - this.description = description; - this.available = available; - this.requestId = requestId != null && requestId <= 0 ? null : requestId; - } } diff --git a/src/main/java/ru/practicum/shareit/item/dto/ItemDtoFullVersion.java b/src/main/java/ru/practicum/shareit/item/dto/ItemDtoFullVersion.java new file mode 100644 index 0000000..caba5a3 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/dto/ItemDtoFullVersion.java @@ -0,0 +1,35 @@ +package ru.practicum.shareit.item.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.NotNull; +import lombok.*; +import lombok.experimental.FieldDefaults; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * TODO Sprint add-controllers. + */ +@Data +@NotNull +@Builder +@FieldDefaults(makeFinal = true) +public class ItemDtoFullVersion { + + Long id; + String name; + String description; + Boolean available; + + @JsonProperty("request") + Long requestId; + + @JsonProperty("lastBooking") + LocalDateTime lastBookingDate; + + @JsonProperty("nextBooking") + LocalDateTime nextBookingDate; + + List comments; +} diff --git a/src/main/java/ru/practicum/shareit/item/dto/ItemMapper.java b/src/main/java/ru/practicum/shareit/item/dto/ItemMapper.java index 8bf3bd6..cfb2ee2 100644 --- a/src/main/java/ru/practicum/shareit/item/dto/ItemMapper.java +++ b/src/main/java/ru/practicum/shareit/item/dto/ItemMapper.java @@ -1,27 +1,51 @@ package ru.practicum.shareit.item.dto; +import org.springframework.lang.NonNull; import ru.practicum.shareit.item.model.Item; -import ru.practicum.shareit.user.User; +import ru.practicum.shareit.user.model.User; + +import java.time.LocalDateTime; +import java.util.List; public class ItemMapper { - public static ItemDto toItemDto(Item item) { - return new ItemDto( - item.getId(), - item.getName(), - item.getDescription(), - item.isAvailable(), - item.getRequest() != null ? item.getRequest().getId() : null - ); + + @NonNull + public static ItemDto toItemDto(@NonNull Item item) { + return ItemDto.builder() + .id(item.getId()) + .name(item.getName()) + .description(item.getDescription()) + .available(item.isAvailable()) + .requestId(item.getRequest() != null ? item.getRequest().getId() : null) + .build(); + } + + public static ItemDtoFullVersion toFullItemDto( + @NonNull Item item, + LocalDateTime lastBookingDate, + LocalDateTime nextBookingDate, + List comments + ) { + return ItemDtoFullVersion.builder() + .id(item.getId()) + .name(item.getName()) + .description(item.getDescription()) + .available(item.isAvailable()) + .requestId(item.getRequest() == null ? null : item.getRequest().getId()) + .lastBookingDate(lastBookingDate) + .nextBookingDate(nextBookingDate) + .comments(comments) + .build(); } - public static Item toItem(ItemDto itemDto, User user) { - return new Item( - itemDto.getId(), - itemDto.getName(), - itemDto.getDescription(), - itemDto.getAvailable(), - user, - null - ); + @NonNull + public static Item toItem(@NonNull ItemDto itemDto, User owner) { + return new Item() + .setId(itemDto.getId()) + .setName(itemDto.getName()) + .setDescription(itemDto.getDescription()) + .setAvailable(itemDto.getAvailable()) + .setOwner(owner) + .setRequest(null); } } diff --git a/src/main/java/ru/practicum/shareit/item/model/Comment.java b/src/main/java/ru/practicum/shareit/item/model/Comment.java new file mode 100644 index 0000000..6c965e9 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/model/Comment.java @@ -0,0 +1,55 @@ +package ru.practicum.shareit.item.model; + +import jakarta.persistence.*; +import lombok.*; +import lombok.experimental.Accessors; +import lombok.experimental.ExtensionMethod; +import ru.practicum.shareit.user.model.User; +import ru.practicum.shareit.util.hibernate.HibernateEqualsAndHashCode; + +import java.time.LocalDateTime; +import java.util.Objects; + +import static ru.practicum.shareit.util.hibernate.HibernateEqualsAndHashCode.*; + +@Data +@Entity +@NoArgsConstructor +@Accessors(chain = true) +@Table(name = "comments") +@ExtensionMethod({HibernateEqualsAndHashCode.class}) +public class Comment { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "item_id") + Item item; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "author_id") + User author; + + LocalDateTime createdDate; + + @Column(name = "comment_text") + String text; + + //region equals and hashCode + @Override + public final boolean equals(Object object) { + return this == object + || object != null + && persistentClass(this) == object.persistentClass() + && object instanceof Comment comment + && Objects.equals(getId(), comment.getId()); + } + + @Override + public final int hashCode() { + return persistentClass(this).hashCode(); + } + //endregion +} 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 92523c2..f17ad6b 100644 --- a/src/main/java/ru/practicum/shareit/item/model/Item.java +++ b/src/main/java/ru/practicum/shareit/item/model/Item.java @@ -1,25 +1,63 @@ package ru.practicum.shareit.item.model; -import jakarta.validation.constraints.NotNull; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.experimental.FieldDefaults; -import ru.practicum.shareit.request.ItemRequest; -import ru.practicum.shareit.user.User; +import jakarta.persistence.*; +import lombok.*; +import lombok.experimental.Accessors; +import lombok.experimental.ExtensionMethod; +import ru.practicum.shareit.request.model.ItemRequest; +import ru.practicum.shareit.user.model.User; +import ru.practicum.shareit.util.hibernate.HibernateEqualsAndHashCode; + +import java.util.Objects; + +import static ru.practicum.shareit.util.hibernate.HibernateEqualsAndHashCode.*; /** * TODO Sprint add-controllers. */ @Data -@NotNull -@AllArgsConstructor -@FieldDefaults(level = AccessLevel.PRIVATE) +@Entity +@NoArgsConstructor +@Accessors(chain = true) +@Table(name = "items") +@ExtensionMethod({HibernateEqualsAndHashCode.class}) public class Item { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) Long id; + + @Column(name = "item_name", nullable = false, length = 100) String name; + + @Column String description; + + /// статус о том, доступна или нет вещь для аренды + @Column(nullable = false) boolean available; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "owner_id", nullable = false) User owner; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "request_id") ItemRequest request; + + //region equals and hashCode + @Override + public final boolean equals(Object object) { + return this == object + || object != null + && persistentClass(this) == object.persistentClass() + && object instanceof Item item + && Objects.equals(getId(), item.getId()); + } + + @Override + public final int hashCode() { + return persistentClass(this).hashCode(); + } + //endregion } diff --git a/src/main/java/ru/practicum/shareit/item/repository/CommentRepository.java b/src/main/java/ru/practicum/shareit/item/repository/CommentRepository.java new file mode 100644 index 0000000..2dc4678 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/repository/CommentRepository.java @@ -0,0 +1,14 @@ +package ru.practicum.shareit.item.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import ru.practicum.shareit.item.model.Comment; + +import java.util.List; +import java.util.Set; + +public interface CommentRepository extends JpaRepository { + + List findAllByItemId(long itemId); + + List findAllByItemIdIn(Set itemIds); +} diff --git a/src/main/java/ru/practicum/shareit/item/repository/FakeItemRepository.java b/src/main/java/ru/practicum/shareit/item/repository/FakeItemRepository.java deleted file mode 100644 index 7679d16..0000000 --- a/src/main/java/ru/practicum/shareit/item/repository/FakeItemRepository.java +++ /dev/null @@ -1,58 +0,0 @@ -package ru.practicum.shareit.item.repository; - -import lombok.AccessLevel; -import lombok.RequiredArgsConstructor; -import lombok.experimental.FieldDefaults; -import org.springframework.stereotype.Repository; -import ru.practicum.shareit.item.model.Item; -import ru.practicum.shareit.util.IdentifyService; -import ru.practicum.shareit.util.stub.MethodNotImplemented; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; - -@Repository -@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) -@RequiredArgsConstructor -public class FakeItemRepository implements ItemRepository { - - Map items = new HashMap<>(); - IdentifyService identifyService; - - @Override - public Optional getOne(long itemId) { - Item item = items.get(itemId); - return item == null ? Optional.empty() : Optional.of(item); - } - - @Override - public List findAll() { - return items.values().stream().toList(); - } - - @Override - public Item save(Item item) { - item.setId(identifyService.getNextId(items)); - items.put(item.getId(), item); - return item; - } - - @Override - public Item update(long itemId, Item newItem) { - items.put(itemId, newItem); - return newItem; - } - - @Override - public boolean checkItemIsNotPresent(long itemId) { - return !items.containsKey(itemId); - } - - @Override - @MethodNotImplemented - public void remove(long userId, long itemId) { - items.remove(itemId); - } -} diff --git a/src/main/java/ru/practicum/shareit/item/repository/ItemRepository.java b/src/main/java/ru/practicum/shareit/item/repository/ItemRepository.java index 10d9d6f..7f1fe38 100644 --- a/src/main/java/ru/practicum/shareit/item/repository/ItemRepository.java +++ b/src/main/java/ru/practicum/shareit/item/repository/ItemRepository.java @@ -1,27 +1,19 @@ 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 java.util.List; -import java.util.Optional; -public interface ItemRepository { +public interface ItemRepository extends JpaRepository { - /// Получить вещь по id - Optional getOne(long itemId); + @Query(" select i from Item i " + + "where (upper(i.name) like upper(concat('%', ?1, '%')) " + + " or upper(i.description) like upper(concat('%', ?1, '%'))) " + + "and i.available = true" + ) + List search(String text); - /// Получить все вещи - List findAll(); - - /// Сохранить новую вещь - Item save(Item item); - - /// Обновить вещь по id - Item update(long itemId, Item newItem); - - /// Проверить что вещи с таким id нет в базе - boolean checkItemIsNotPresent(long itemId); - - /// Удалить вещь по id - void remove(long userId, long itemId); + List findAllByOwner_Id(Long ownerId); } 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 389eee0..81e1582 100644 --- a/src/main/java/ru/practicum/shareit/item/service/ItemService.java +++ b/src/main/java/ru/practicum/shareit/item/service/ItemService.java @@ -1,26 +1,38 @@ package ru.practicum.shareit.item.service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.shareit.item.dto.CommentDto; import ru.practicum.shareit.item.dto.ItemDto; +import ru.practicum.shareit.item.dto.ItemDtoFullVersion; import java.util.List; +@Transactional(readOnly = true) public interface ItemService { /// Получить вещь по id - ItemDto getItem(long itemId); + ItemDtoFullVersion getItem(long userId, long itemId); /// Получить все вещи - List getAllItemsOfUser(long userId); + List getAllItemsOfUser(long userId); /// Поиск вещей по подстроке List search(long userId, String search); /// Добавить новую вещь + @Transactional ItemDto addNewItem(Long userId, ItemDto itemDto); + /// Обновить данные вещи по id + @Transactional(propagation = Propagation.REQUIRED) + ItemDto updateItem(long userId, long itemId, ItemDto itemDto); + /// Удалить вещь по id + @Transactional void deleteItem(long userId, long itemId); - /// Обновить данные вещи по id - ItemDto updateItem(long userId, long itemId, ItemDto itemDto); + /// Добавить комментарий + @Transactional + CommentDto addComment(long userId, long itemId, CommentDto commentDto); } 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 322c508..c91badc 100644 --- a/src/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java +++ b/src/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java @@ -1,41 +1,85 @@ package ru.practicum.shareit.item.service; -import lombok.AccessLevel; import lombok.RequiredArgsConstructor; import lombok.experimental.FieldDefaults; +import org.springframework.lang.NonNull; import org.springframework.stereotype.Service; -import ru.practicum.shareit.item.dto.ItemDto; -import ru.practicum.shareit.item.dto.ItemMapper; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.repository.BookingRepository; +import ru.practicum.shareit.item.dto.*; +import ru.practicum.shareit.item.model.Comment; import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.item.repository.CommentRepository; import ru.practicum.shareit.item.repository.ItemRepository; -import ru.practicum.shareit.user.User; +import ru.practicum.shareit.user.model.User; import ru.practicum.shareit.user.repository.UserRepository; +import ru.practicum.shareit.util.exception.ForbiddenException; import ru.practicum.shareit.util.exception.NotFoundException; +import ru.practicum.shareit.util.exception.ParameterNotValidException; +import java.time.LocalDateTime; +import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; @Service -@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) @RequiredArgsConstructor +@FieldDefaults(makeFinal = true) public class ItemServiceImpl implements ItemService { ItemRepository itemRepository; UserRepository userRepository; + BookingRepository bookingRepository; + CommentRepository commentRepository; @Override - public ItemDto getItem(long itemId) { - return ItemMapper.toItemDto(getItemWithCheckPresent(itemId)); + public ItemDtoFullVersion getItem(long userId, long itemId) { + Item item = getItemWithCheckPresent(itemId); + + if (item.getOwner().getId() != userId) { + return ItemMapper.toFullItemDto( + item, + null, + null, + getCommentDtos(itemId) + ); + } + return ItemMapper.toFullItemDto( + item, + bookingRepository.findLastBookingDate(itemId), + bookingRepository.findNextBookingDate(itemId), + getCommentDtos(itemId) + ); } @Override - public List getAllItemsOfUser(long userId) { + public List getAllItemsOfUser(long userId) { checkUser(userId); + Map items = itemRepository.findAllByOwner_Id(userId) + .stream() + .collect(Collectors.toMap(Item::getId, Function.identity())); + List bookings = bookingRepository.findAllBookingByItemIdIn(items.keySet()); + Map comments = commentRepository.findAllByItemIdIn(items.keySet()).stream() + .collect(Collectors.toMap(comment -> comment.getItem().getId(), Function.identity())); + + List result = new ArrayList<>(); + for (Item item : items.values()) { + result.add(ItemMapper.toFullItemDto( + item, + getLastBookingDate(bookings), + getNextBookingDate(bookings), + comments.entrySet().stream() + .filter(entry -> entry.getKey().equals(item.getId())) + .map(Map.Entry::getValue) + .map(CommentMapper::toDto) + .toList() + )); + } - return itemRepository.findAll().stream() - .filter(item -> item.getOwner().getId() == userId) - .map(ItemMapper::toItemDto) - .toList(); + return result; } @Override @@ -43,12 +87,7 @@ public List search(long userId, String text) { if (text == null || text.isEmpty()) { return Collections.emptyList(); } - String searchText = text.toLowerCase(); - return itemRepository.findAll().stream() - .filter(Item::isAvailable) - .filter(item -> item.getName().toLowerCase().contains(searchText) || - item.getDescription().toLowerCase().contains(searchText) - ) + return itemRepository.search(text).stream() .map(ItemMapper::toItemDto) .toList(); } @@ -60,46 +99,68 @@ public ItemDto addNewItem(Long userId, ItemDto itemDto) { } @Override - public ItemDto updateItem(long userId, long itemId, ItemDto itemDto) { + public ItemDto updateItem(long userId, long itemId, @NonNull ItemDto itemDto) { checkUser(userId); Item oldItem = getItemWithCheckPresentAndOwner(itemId, userId); - - Item item = new Item( - itemId, - itemDto.getName() == null ? oldItem.getName() : itemDto.getName(), - itemDto.getDescription() == null ? oldItem.getDescription() : itemDto.getDescription(), - itemDto.getAvailable() == null ? oldItem.isAvailable() : itemDto.getAvailable(), - getUserWithCheckPresent(userId), - null - ); - return ItemMapper.toItemDto(itemRepository.update(itemId, item)); + Item item = new Item() + .setId(itemId) + .setName(itemDto.getName() == null ? oldItem.getName() : itemDto.getName()) + .setDescription(itemDto.getDescription() == null ? oldItem.getDescription() : itemDto.getDescription()) + .setAvailable(itemDto.getAvailable() == null ? oldItem.isAvailable() : itemDto.getAvailable()) + .setOwner(getUserWithCheckPresent(userId)) + .setRequest(null); + return ItemMapper.toItemDto(itemRepository.save(item)); } @Override public void deleteItem(long userId, long itemId) { checkUser(userId); getItemWithCheckPresentAndOwner(itemId, userId); - itemRepository.remove(userId, itemId); + itemRepository.deleteById(itemId); + } + + @Override + public CommentDto addComment(long userId, long itemId, @NonNull CommentDto commentDto) { + User user = getUserWithCheckPresent(userId); + Item item = getItemWithCheckPresent(itemId); + if (item.getOwner().getId().equals(userId)) { + throw new ForbiddenException("Нельзя оставить комментарий к своей вещи"); + } + if (bookingRepository.findAllBookingByItem_Id(item.getId()).stream() + .noneMatch(booking -> booking.getBooker().getId().equals(userId) && + booking.getEnd().isBefore(LocalDateTime.now())) + ) { + throw new ParameterNotValidException( + "userId", "Комментарии доступны только тем, кто уже по пользовался вещью" + ); + } + return CommentMapper.toDto(commentRepository.save( + CommentMapper.toComment(commentDto, item, user) + ) + ); } private void checkUser(long userId) { - if (userRepository.checkUserIsNotPresent(userId)) { + if (!userRepository.existsById(userId)) { throw new NotFoundException("Пользователь с id=" + userId + " не найден."); } } + @NonNull private User getUserWithCheckPresent(long userId) { - return userRepository.findOne(userId).orElseThrow(() -> + return userRepository.findById(userId).orElseThrow(() -> new NotFoundException("Пользователь с id=" + userId + " не найден.") ); } + @NonNull private Item getItemWithCheckPresent(long itemId) { - return itemRepository.getOne(itemId).orElseThrow(() -> + return itemRepository.findById(itemId).orElseThrow(() -> new NotFoundException("Вещь с id=" + itemId + " не найдена.") ); } + @NonNull private Item getItemWithCheckPresentAndOwner(long itemId, long userId) { Item item = getItemWithCheckPresent(itemId); if (item.getOwner().getId() == userId) { @@ -107,4 +168,28 @@ private Item getItemWithCheckPresentAndOwner(long itemId, long userId) { } throw new NotFoundException("Пользователь с id=" + userId + " не является владельцем вещи с id=" + itemId); } + + private LocalDateTime getLastBookingDate(@NonNull List bookings) { + LocalDateTime now = LocalDateTime.now(); + return bookings.stream() + .map(Booking::getStart) + .filter(start -> !start.isAfter(now)) + .max(LocalDateTime::compareTo) + .orElse(null); + } + + private LocalDateTime getNextBookingDate(@NonNull List bookings) { + LocalDateTime now = LocalDateTime.now(); + return bookings.stream() + .map(Booking::getStart) + .filter(start -> start.isAfter(now)) + .min(LocalDateTime::compareTo) + .orElse(null); + } + + private List getCommentDtos(long itemId) { + return commentRepository.findAllByItemId(itemId).stream() + .map(CommentMapper::toDto) + .toList(); + } } diff --git a/src/main/java/ru/practicum/shareit/request/ItemRequest.java b/src/main/java/ru/practicum/shareit/request/ItemRequest.java deleted file mode 100644 index b6453d8..0000000 --- a/src/main/java/ru/practicum/shareit/request/ItemRequest.java +++ /dev/null @@ -1,20 +0,0 @@ -package ru.practicum.shareit.request; - -import lombok.AccessLevel; -import lombok.Data; -import lombok.experimental.FieldDefaults; -import ru.practicum.shareit.user.User; - -import java.time.LocalDateTime; - -/** - * TODO Sprint add-item-requests. - */ -@Data -@FieldDefaults(level = AccessLevel.PRIVATE) -public class ItemRequest { - long id; - String description; - User requestor; - LocalDateTime created; -} diff --git a/src/main/java/ru/practicum/shareit/request/model/ItemRequest.java b/src/main/java/ru/practicum/shareit/request/model/ItemRequest.java new file mode 100644 index 0000000..2bb0df7 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/request/model/ItemRequest.java @@ -0,0 +1,35 @@ +package ru.practicum.shareit.request.model; + +import jakarta.persistence.*; +import lombok.*; +import lombok.experimental.Accessors; +import lombok.experimental.ExtensionMethod; +import ru.practicum.shareit.user.model.User; +import ru.practicum.shareit.util.hibernate.HibernateEqualsAndHashCode; + +import java.time.LocalDateTime; + +/** + * TODO Sprint add-item-requests. + */ +@Data +@Entity +@Table(name = "requests") +@NoArgsConstructor +@Accessors(chain = true) +@ExtensionMethod({HibernateEqualsAndHashCode.class}) +public class ItemRequest { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + long id; + + String description; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "requestor_id", nullable = false) + User requestor; + + @Column(nullable = false) + LocalDateTime created; +} diff --git a/src/main/java/ru/practicum/shareit/request/repository/FakeItemRequestRepository.java b/src/main/java/ru/practicum/shareit/request/repository/FakeItemRequestRepository.java deleted file mode 100644 index 8635fdb..0000000 --- a/src/main/java/ru/practicum/shareit/request/repository/FakeItemRequestRepository.java +++ /dev/null @@ -1,4 +0,0 @@ -package ru.practicum.shareit.request.repository; - -public class FakeItemRequestRepository implements ItemRequestRepository { -} diff --git a/src/main/java/ru/practicum/shareit/request/repository/ItemRequestRepository.java b/src/main/java/ru/practicum/shareit/request/repository/ItemRequestRepository.java index ea6045f..6d136f3 100644 --- a/src/main/java/ru/practicum/shareit/request/repository/ItemRequestRepository.java +++ b/src/main/java/ru/practicum/shareit/request/repository/ItemRequestRepository.java @@ -1,4 +1,7 @@ package ru.practicum.shareit.request.repository; -public interface ItemRequestRepository { +import org.springframework.data.jpa.repository.JpaRepository; +import ru.practicum.shareit.request.model.ItemRequest; + +public interface ItemRequestRepository extends JpaRepository { } diff --git a/src/main/java/ru/practicum/shareit/user/User.java b/src/main/java/ru/practicum/shareit/user/User.java deleted file mode 100644 index bf9ffb2..0000000 --- a/src/main/java/ru/practicum/shareit/user/User.java +++ /dev/null @@ -1,18 +0,0 @@ -package ru.practicum.shareit.user; - -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.experimental.FieldDefaults; - -/** - * TODO Sprint add-controllers. - */ -@Data -@AllArgsConstructor -@FieldDefaults(level = AccessLevel.PRIVATE) -public class User { - Long id; - String name; - String email; -} diff --git a/src/main/java/ru/practicum/shareit/user/UserController.java b/src/main/java/ru/practicum/shareit/user/UserController.java index e1e4ae7..8dec375 100644 --- a/src/main/java/ru/practicum/shareit/user/UserController.java +++ b/src/main/java/ru/practicum/shareit/user/UserController.java @@ -18,7 +18,8 @@ @RequestMapping(path = "/users") @RequiredArgsConstructor public class UserController { - private final UserService userService; + + final UserService userService; @GetMapping("/{userId}") public UserDto getUser(@PathVariable long userId) { 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 b0a4b0a..d538b0b 100644 --- a/src/main/java/ru/practicum/shareit/user/dto/UserDto.java +++ b/src/main/java/ru/practicum/shareit/user/dto/UserDto.java @@ -1,30 +1,25 @@ package ru.practicum.shareit.user.dto; import jakarta.validation.constraints.*; -import lombok.AccessLevel; +import lombok.Builder; import lombok.Data; import lombok.experimental.FieldDefaults; import ru.practicum.shareit.util.Marker; @Data @NotNull -@FieldDefaults(level = AccessLevel.PRIVATE) +@Builder +@FieldDefaults(makeFinal = true) public class UserDto { Long id; @NotBlank(groups = Marker.OnCreate.class) - @Size(min = 1, max = 100) + @Size(min = 1, max = 255) String name; @NotBlank(groups = Marker.OnCreate.class) - @Size(min = 1, max = 100) + @Size(min = 1, max = 512) @Email String email; - - public UserDto(Long id, String name, String email) { - this.id = id != null && id <= 0 ? null : id; - this.name = name == null ? null : name.trim(); - this.email = email == null ? null : email.trim(); - } } diff --git a/src/main/java/ru/practicum/shareit/user/dto/UserMapper.java b/src/main/java/ru/practicum/shareit/user/dto/UserMapper.java index 2e0ca25..3a47070 100644 --- a/src/main/java/ru/practicum/shareit/user/dto/UserMapper.java +++ b/src/main/java/ru/practicum/shareit/user/dto/UserMapper.java @@ -1,22 +1,21 @@ package ru.practicum.shareit.user.dto; -import ru.practicum.shareit.user.User; +import ru.practicum.shareit.user.model.User; public class UserMapper { public static UserDto toUserDto(User user) { - return new UserDto( - user.getId(), - user.getName(), - user.getEmail() - ); + return UserDto.builder() + .id(user.getId()) + .name(user.getName()) + .email(user.getEmail()) + .build(); } public static User toUser(UserDto userDto) { - return new User( - userDto.getId(), - userDto.getName(), - userDto.getEmail() - ); + return new User() + .setId(userDto.getId()) + .setName(userDto.getName()) + .setEmail(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 new file mode 100644 index 0000000..c719c35 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/user/model/User.java @@ -0,0 +1,46 @@ +package ru.practicum.shareit.user.model; + +import jakarta.persistence.*; +import lombok.*; +import lombok.experimental.Accessors; +import lombok.experimental.ExtensionMethod; +import ru.practicum.shareit.util.hibernate.HibernateEqualsAndHashCode; + +import java.util.Objects; + +import static ru.practicum.shareit.util.hibernate.HibernateEqualsAndHashCode.*; + +@Data +@Entity +@Table(name = "users") +@NoArgsConstructor +@Accessors(chain = true) +@ExtensionMethod({HibernateEqualsAndHashCode.class}) +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + Long id; + + @Column(name = "user_name", nullable = false) + String name; + + @Column(nullable = false, unique = true, length = 512) + String email; + + //region equals and hashCode + @Override + public final boolean equals(Object object) { + return this == object + || object != null + && persistentClass(this) == object.persistentClass() + && object instanceof User user + && Objects.equals(getId(), user.getId()); + } + + @Override + public final int hashCode() { + return persistentClass(this).hashCode(); + } + //endregion +} diff --git a/src/main/java/ru/practicum/shareit/user/repository/FakeUserRepository.java b/src/main/java/ru/practicum/shareit/user/repository/FakeUserRepository.java deleted file mode 100644 index d47bae1..0000000 --- a/src/main/java/ru/practicum/shareit/user/repository/FakeUserRepository.java +++ /dev/null @@ -1,62 +0,0 @@ -package ru.practicum.shareit.user.repository; - -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Repository; -import ru.practicum.shareit.user.User; -import ru.practicum.shareit.util.IdentifyService; - -import java.util.*; - -@Repository -@RequiredArgsConstructor -public class FakeUserRepository implements UserRepository { - - private final Map users = new HashMap<>(); - - private final IdentifyService identifyService; - - @Override - public Optional findOne(long userId) { - User user = users.get(userId); - return user == null ? Optional.empty() : Optional.of(user); - } - - @Override - public Collection findAll() { - return users.values(); - } - - @Override - public User save(User user) { - user.setId(identifyService.getNextId(users)); - users.put(user.getId(), user); - return user; - } - - @Override - public User update(User newUser) { - users.remove(newUser.getId()); - users.put(newUser.getId(), newUser); - return newUser; - } - - @Override - public boolean checkUserIsNotPresent(long userId) { - return !users.containsKey(userId); - } - - @Override - public boolean checkEmailIsDuplicated(String email) { - for (User u : users.values()) { - if (u.getEmail().equals(email)) { - return true; - } - } - return false; - } - - @Override - public void remove(long userId) { - users.remove(userId); - } -} diff --git a/src/main/java/ru/practicum/shareit/user/repository/UserRepository.java b/src/main/java/ru/practicum/shareit/user/repository/UserRepository.java index 53d253a..cedef42 100644 --- a/src/main/java/ru/practicum/shareit/user/repository/UserRepository.java +++ b/src/main/java/ru/practicum/shareit/user/repository/UserRepository.java @@ -1,30 +1,16 @@ package ru.practicum.shareit.user.repository; -import ru.practicum.shareit.user.User; - -import java.util.Collection; -import java.util.Optional; - -public interface UserRepository { - - /// Получить одного пользователя если есть - Optional findOne(long userId); - - /// Получить всех пользователей - Collection findAll(); - - /// Сохранить пользовател в базу - User save(User user); - - /// Обновить данные пользователя по id - User update(User newUser); - - /// Проверить что пользователя с таким id нет в базе - boolean checkUserIsNotPresent(long userId); - - /// Проверить что этот пользователь с такой почтой уже есть в базе - boolean checkEmailIsDuplicated(String email); - - /// Удалить пользователя по id - void remove(long userId); +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; +import ru.practicum.shareit.user.model.User; + +@Repository +public interface UserRepository extends JpaRepository { + boolean existsByEmail(String email); + + @Modifying + @Query("delete from User u where u.id = :userId") + int deleteUserById(long userId); } diff --git a/src/main/java/ru/practicum/shareit/user/service/UserService.java b/src/main/java/ru/practicum/shareit/user/service/UserService.java index 738c9c7..45ad6ad 100644 --- a/src/main/java/ru/practicum/shareit/user/service/UserService.java +++ b/src/main/java/ru/practicum/shareit/user/service/UserService.java @@ -1,21 +1,27 @@ package ru.practicum.shareit.user.service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; import ru.practicum.shareit.user.dto.UserDto; import java.util.List; +@Transactional(readOnly = true) public interface UserService { /// Получить DTO всех пользователей List getAllUsers(); /// Создать и сохранить пользователя в базу + @Transactional UserDto saveUser(UserDto userDto); /// Удалить пользователя по id + @Transactional void deleteUser(long userId); /// Обновить данные пользователя по id + @Transactional(propagation = Propagation.REQUIRED) UserDto updateUser(long userId, UserDto userDto); /// Получчить пользователя по id 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 8f9deef..6bfc640 100644 --- a/src/main/java/ru/practicum/shareit/user/service/UserServiceImpl.java +++ b/src/main/java/ru/practicum/shareit/user/service/UserServiceImpl.java @@ -2,7 +2,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import ru.practicum.shareit.user.User; +import ru.practicum.shareit.user.model.User; import ru.practicum.shareit.user.dto.UserDto; import ru.practicum.shareit.user.dto.UserMapper; import ru.practicum.shareit.user.repository.UserRepository; @@ -15,7 +15,7 @@ @RequiredArgsConstructor class UserServiceImpl implements UserService { - private final UserRepository userRepository; + final UserRepository userRepository; @Override public UserDto getUser(long userId) { @@ -45,25 +45,27 @@ public UserDto updateUser(long userId, UserDto userDto) { checkEmail(userDto.getEmail()); user.setEmail(userDto.getEmail()); } - return UserMapper.toUserDto(userRepository.update(user)); + return UserMapper.toUserDto(userRepository.save(user)); } @Override public void deleteUser(long userId) { - if (userRepository.checkUserIsNotPresent(userId)) { + if (!userRepository.existsById(userId)) { throw new NotFoundException("Пользователь с id=" + userId + " не найден."); } - userRepository.remove(userId); + if (userRepository.deleteUserById(userId) < 1) { + throw new RuntimeException("Удаление не удалось"); + } } private void checkEmail(String email) { - if (userRepository.checkEmailIsDuplicated(email)) { + if (userRepository.existsByEmail(email)) { throw new DuplicatedDataException("Пользователь с такой почтой " + email + " уже есть."); } } private User getUserWithCheckPresent(long userId) { - return userRepository.findOne(userId).orElseThrow(() -> + return userRepository.findById(userId).orElseThrow(() -> new NotFoundException("Пользователь с id=" + userId + " не найден.") ); } diff --git a/src/main/java/ru/practicum/shareit/util/IdentifyService.java b/src/main/java/ru/practicum/shareit/util/IdentifyService.java deleted file mode 100644 index 1386601..0000000 --- a/src/main/java/ru/practicum/shareit/util/IdentifyService.java +++ /dev/null @@ -1,20 +0,0 @@ -package ru.practicum.shareit.util; - -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; - -import java.util.Map; - -@Service -@Slf4j -public class IdentifyService { - public long getNextId(Map map) { - log.trace("Генерация нового id"); - int currentMaxId = map.keySet() - .stream() - .mapToInt(Math::toIntExact) - .max() - .orElse(0); - return ++currentMaxId; - } -} diff --git a/src/main/java/ru/practicum/shareit/util/Marker.java b/src/main/java/ru/practicum/shareit/util/Marker.java index 9abf870..6d03882 100644 --- a/src/main/java/ru/practicum/shareit/util/Marker.java +++ b/src/main/java/ru/practicum/shareit/util/Marker.java @@ -1,9 +1,5 @@ package ru.practicum.shareit.util; public interface Marker { - interface OnCreate {} - - interface OnUpdate {} - } diff --git a/src/main/java/ru/practicum/shareit/util/config/WebConfig.java b/src/main/java/ru/practicum/shareit/util/config/WebConfig.java deleted file mode 100644 index ef0380a..0000000 --- a/src/main/java/ru/practicum/shareit/util/config/WebConfig.java +++ /dev/null @@ -1,8 +0,0 @@ -package ru.practicum.shareit.util.config; - -import org.springframework.context.annotation.Configuration; - -@Configuration -public class WebConfig { - -} diff --git a/src/main/java/ru/practicum/shareit/util/datetime/FutureOrPresentWithTolerance.java b/src/main/java/ru/practicum/shareit/util/datetime/FutureOrPresentWithTolerance.java new file mode 100644 index 0000000..79857d6 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/util/datetime/FutureOrPresentWithTolerance.java @@ -0,0 +1,26 @@ +package ru.practicum.shareit.util.datetime; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.*; + +/// Generated by GPT +@Documented +@Constraint(validatedBy = FutureOrPresentWithToleranceValidator.class) +@Target({ ElementType.FIELD, ElementType.PARAMETER }) +@Retention(RetentionPolicy.RUNTIME) +public @interface FutureOrPresentWithTolerance { + + String message() default "must be in the present or future (with tolerance)"; + + Class[] groups() default {}; + + Class[] payload() default {}; + + long toleranceMillis() default 0; + + String zone() default ""; + + TimeRoundingStrategy rounding() default TimeRoundingStrategy.NONE; +} diff --git a/src/main/java/ru/practicum/shareit/util/datetime/FutureOrPresentWithToleranceValidator.java b/src/main/java/ru/practicum/shareit/util/datetime/FutureOrPresentWithToleranceValidator.java new file mode 100644 index 0000000..e4147a6 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/util/datetime/FutureOrPresentWithToleranceValidator.java @@ -0,0 +1,96 @@ +package ru.practicum.shareit.util.datetime; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +import java.time.*; + +/// Generated by GPT +public class FutureOrPresentWithToleranceValidator + implements ConstraintValidator { + + long toleranceMillis; + String zoneIdStr; + TimeRoundingStrategy roundingStrategy; + + @Override + public void initialize(FutureOrPresentWithTolerance constraintAnnotation) { + this.toleranceMillis = constraintAnnotation.toleranceMillis(); + this.zoneIdStr = constraintAnnotation.zone(); + this.roundingStrategy = constraintAnnotation.rounding(); + } + + @Override + public boolean isValid(Object value, ConstraintValidatorContext context) { + if (value == null) { + return true; + } + + Clock clock = context.getClockProvider().getClock(); + + ZoneId zone = resolveZone(clock); + + Instant now = applyRounding(clock.instant()); + Instant valueInstant = toInstant(value, zone); + + if (valueInstant == null) { + return false; + } + + valueInstant = applyRounding(valueInstant); + + return valueInstant.toEpochMilli() >= (now.toEpochMilli() - toleranceMillis); + } + + private ZoneId resolveZone(Clock clock) { + if (zoneIdStr == null || zoneIdStr.isBlank()) { + return clock.getZone(); + } + return ZoneId.of(zoneIdStr); + } + + private Instant toInstant(Object value, ZoneId zone) { + + if (value instanceof Instant) { + return (Instant) value; + } + + if (value instanceof OffsetDateTime) { + return ((OffsetDateTime) value).toInstant(); + } + + if (value instanceof ZonedDateTime) { + return ((ZonedDateTime) value).toInstant(); + } + + if (value instanceof LocalDateTime) { + return ((LocalDateTime) value).atZone(zone).toInstant(); + } + + if (value instanceof LocalDate) { + return ((LocalDate) value).atStartOfDay(zone).toInstant(); + } + + return null; + } + + private Instant applyRounding(Instant instant) { + + switch (roundingStrategy) { + + case TRUNCATE_TO_SECONDS: + return instant.truncatedTo(java.time.temporal.ChronoUnit.SECONDS); + + case TRUNCATE_TO_MINUTES: + return instant.truncatedTo(java.time.temporal.ChronoUnit.MINUTES); + + case ROUND_TO_SECONDS: + long millis = instant.toEpochMilli(); + long rounded = Math.round(millis / 1000.0) * 1000; + return Instant.ofEpochMilli(rounded); + + default: + return instant; + } + } +} diff --git a/src/main/java/ru/practicum/shareit/util/datetime/TimeRoundingStrategy.java b/src/main/java/ru/practicum/shareit/util/datetime/TimeRoundingStrategy.java new file mode 100644 index 0000000..6ef8f41 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/util/datetime/TimeRoundingStrategy.java @@ -0,0 +1,9 @@ +package ru.practicum.shareit.util.datetime; + +public enum TimeRoundingStrategy { + + NONE, // без округления + TRUNCATE_TO_SECONDS, // обрезать до секунд + TRUNCATE_TO_MINUTES, // обрезать до минут + ROUND_TO_SECONDS // математическое округление +} diff --git a/src/main/java/ru/practicum/shareit/util/error/ErrorHandler.java b/src/main/java/ru/practicum/shareit/util/error/ErrorHandler.java index 823eb78..6b9f82b 100644 --- a/src/main/java/ru/practicum/shareit/util/error/ErrorHandler.java +++ b/src/main/java/ru/practicum/shareit/util/error/ErrorHandler.java @@ -14,9 +14,7 @@ import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; -import ru.practicum.shareit.util.exception.DuplicatedDataException; -import ru.practicum.shareit.util.exception.MethodNotImplementedException; -import ru.practicum.shareit.util.exception.NotFoundException; +import ru.practicum.shareit.util.exception.*; import java.util.HashMap; import java.util.Map; @@ -38,6 +36,26 @@ public ErrorResponse handleNotFound(final NotFoundException e) { return new ErrorResponse(e.getMessage()); } + @ResponseStatus(HttpStatus.FORBIDDEN) + @ExceptionHandler + public ErrorResponse handleSharingForbidden(final ForbiddenException e) { + return new ErrorResponse(e.getMessage()); + } + + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler + public ErrorResponse handleParameterNotValid(final ParameterNotValidException e) { + return new ErrorResponse("Некорректное значение параметра " + + e.getParameter() + ": " + e.getReason() + ); + } + + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler + public ErrorResponse handleBookingStatusException(final BookingStatusException e) { + return new ErrorResponse(e.getMessage()); + } + @ResponseStatus(HttpStatus.CONFLICT) @ExceptionHandler public ErrorResponse handleDuplicatedData(final DuplicatedDataException e) { diff --git a/src/main/java/ru/practicum/shareit/util/exception/BookingStatusException.java b/src/main/java/ru/practicum/shareit/util/exception/BookingStatusException.java new file mode 100644 index 0000000..139769b --- /dev/null +++ b/src/main/java/ru/practicum/shareit/util/exception/BookingStatusException.java @@ -0,0 +1,7 @@ +package ru.practicum.shareit.util.exception; + +public class BookingStatusException extends RuntimeException { + public BookingStatusException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/practicum/shareit/util/exception/ConditionsNotMetException.java b/src/main/java/ru/practicum/shareit/util/exception/ConditionsNotMetException.java new file mode 100644 index 0000000..a49d237 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/util/exception/ConditionsNotMetException.java @@ -0,0 +1,7 @@ +package ru.practicum.shareit.util.exception; + +public class ConditionsNotMetException extends RuntimeException { + public ConditionsNotMetException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/practicum/shareit/util/exception/ForbiddenException.java b/src/main/java/ru/practicum/shareit/util/exception/ForbiddenException.java new file mode 100644 index 0000000..d3fbbc0 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/util/exception/ForbiddenException.java @@ -0,0 +1,7 @@ +package ru.practicum.shareit.util.exception; + +public class ForbiddenException extends RuntimeException { + public ForbiddenException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/practicum/shareit/util/exception/ParameterNotValidException.java b/src/main/java/ru/practicum/shareit/util/exception/ParameterNotValidException.java new file mode 100644 index 0000000..9c3dc73 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/util/exception/ParameterNotValidException.java @@ -0,0 +1,16 @@ +package ru.practicum.shareit.util.exception; + +import lombok.Getter; + +@Getter +public class ParameterNotValidException extends IllegalArgumentException { + String parameter; + String reason; + + public ParameterNotValidException(String parameter, String reason) { + super(reason); + this.parameter = parameter; + this.reason = reason; + } +} + diff --git a/src/main/java/ru/practicum/shareit/util/hibernate/HibernateEqualsAndHashCode.java b/src/main/java/ru/practicum/shareit/util/hibernate/HibernateEqualsAndHashCode.java new file mode 100644 index 0000000..298c12d --- /dev/null +++ b/src/main/java/ru/practicum/shareit/util/hibernate/HibernateEqualsAndHashCode.java @@ -0,0 +1,13 @@ +package ru.practicum.shareit.util.hibernate; + +import lombok.experimental.UtilityClass; +import org.hibernate.proxy.HibernateProxy; + +@UtilityClass +public class HibernateEqualsAndHashCode { + public Class persistentClass(Object object) { + return object instanceof HibernateProxy proxy ? + proxy.getHibernateLazyInitializer().getPersistentClass() + : object.getClass(); + } +} diff --git a/src/main/resources/application-test.properties b/src/main/resources/application-test.properties index 9e9bc4b..7095f54 100644 --- a/src/main/resources/application-test.properties +++ b/src/main/resources/application-test.properties @@ -6,8 +6,7 @@ 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 H2 DB -#spring.datasource.driverClassName -#spring.datasource.url -#spring.datasource.username -#spring.datasource.password +spring.datasource.driver-class-name=org.h2.Driver +spring.datasource.url=jdbc:h2:file:./db/filmorate +spring.datasource.username=password +spring.datasource.password=sa diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 9d2c2c2..46af283 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,18 +1,19 @@ -spring.jpa.hibernate.ddl-auto=none -spring.jpa.properties.hibernate.format_sql=true +spring.main.log-startup-info=false +spring.jackson.serialization.indent_output=true spring.sql.init.mode=always +spring.jpa.hibernate.ddl-auto=validate +spring.jpa.properties.hibernate.format_sql=true -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 -#logging.level.org.springframework.web=DEBUG -logging.level.org.zalando.logbook=TRACE +spring.datasource.driver-class-name=org.postgresql.Driver +spring.datasource.url=jdbc:postgresql://localhost:5432/shareit +spring.datasource.username=dbuser +spring.datasource.password=12345 -spring.jackson.serialization.indent_output=true +hibernate.jdbc.time-zone=UTC +hibernate.default-schema=public +hibernate.show-sql=true +hibernate.format-sql=true -# TODO Append connection to Postgres DB -#spring.datasource.driverClassName -#spring.datasource.url -#spring.datasource.username -#spring.datasource.password +logging.level.org.hibernate.SQL=DEBUG +logging.level.org.hibernate.orm.jdbc.bind=trace +logging.level.org.zalando.logbook=TRACE diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql new file mode 100644 index 0000000..2763290 --- /dev/null +++ b/src/main/resources/schema.sql @@ -0,0 +1,54 @@ +CREATE TABLE IF NOT EXISTS public.users ( + id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL, + user_name VARCHAR(255) NOT NULL, + email VARCHAR(512) NOT NULL, + CONSTRAINT pk_users PRIMARY KEY (id) +); +CREATE UNIQUE INDEX IF NOT EXISTS idx_users_email ON users (email); + +CREATE TABLE IF NOT EXISTS public.requests ( + id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL, + description varchar(255) NULL, + requestor_id int8 NOT NULL, + created timestamp without time zone NOT NULL, + CONSTRAINT pk_requests PRIMARY KEY (id), + CONSTRAINT fk_requests_users_id FOREIGN KEY (requestor_id) REFERENCES public.users(id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS public.items ( + id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL, + item_name varchar(100) NOT NULL, + description varchar(255) NULL, + available bool NOT NULL, + owner_id int8 NULL, + request_id int8 NULL, + CONSTRAINT pk_items PRIMARY KEY (id), + CONSTRAINT fk_items_users_id FOREIGN KEY (owner_id) REFERENCES public.users(id) ON DELETE CASCADE, + CONSTRAINT fk_items_requests_id FOREIGN KEY (request_id) REFERENCES public.requests(id) ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS idx_items_name ON items(item_name); +CREATE INDEX IF NOT EXISTS idx_items_description ON items(description); + +CREATE TABLE IF NOT EXISTS public.bookings ( + id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL, + start_date timestamp without time zone NOT NULL NOT NULL, + end_date timestamp without time zone NOT NULL, + item_id bigint NOT NULL, + booker_id bigint NOT NULL, + status varchar(255) check (status in ('WAITING','APPROVED','REJECTED','CANCELED')), + CONSTRAINT pk_bookings PRIMARY KEY (id), + CONSTRAINT fk_bookings_items_id FOREIGN KEY (item_id) REFERENCES public.items(id) ON DELETE CASCADE, + CONSTRAINT fk_bookings_users_id FOREIGN KEY (booker_id) REFERENCES public.users(id) ON DELETE CASCADE + ); + CREATE INDEX IF NOT EXISTS idx_booking_start_end ON bookings(start_date, end_date); + +CREATE TABLE IF NOT EXISTS public.comments ( + id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL, + item_id bigint NOT NULL, + author_id bigint NOT NULL, + created_date timestamp without time zone NOT NULL, + comment_text VARCHAR(255) NOT NULL, + CONSTRAINT pk_comments PRIMARY KEY (id), + CONSTRAINT fk_comments_items_id FOREIGN KEY (item_id) REFERENCES public.items(id) ON DELETE CASCADE, + CONSTRAINT fk_comments_users_id FOREIGN KEY (item_id) REFERENCES public.users(id) ON DELETE CASCADE +);