From 6f5595f31207544963122f8fcfc785bf01610cb6 Mon Sep 17 00:00:00 2001 From: AleksOBM Date: Sun, 22 Mar 2026 23:33:51 +0300 Subject: [PATCH 01/12] =?UTF-8?q?=D0=9F=D0=BE=D0=B4=D0=BA=D0=BB=D1=8E?= =?UTF-8?q?=D1=87=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BA=20=D0=91=D0=94=20=D0=B8?= =?UTF-8?q?=20=D0=BD=D0=B0=D1=81=D1=82=D1=80=D0=BE=D0=B9=D0=BA=D0=B0=20?= =?UTF-8?q?=D1=82=D0=B0=D0=B1=D0=BB=D0=B8=D1=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- compose.yaml | 45 +++++++++++++++++++ pom.xml | 34 +++++--------- .../ru/practicum/shareit/booking/Booking.java | 22 +++++++++ .../practicum/shareit/item/dto/ItemDto.java | 4 +- .../ru/practicum/shareit/item/model/Item.java | 22 ++++++++- .../shareit/request/ItemRequest.java | 16 +++++++ .../java/ru/practicum/shareit/user/User.java | 18 +++++--- .../practicum/shareit/user/dto/UserDto.java | 4 +- src/main/resources/application.properties | 32 ++++++++----- src/main/resources/schema.sql | 40 +++++++++++++++++ 10 files changed, 193 insertions(+), 44 deletions(-) create mode 100644 compose.yaml create mode 100644 src/main/resources/schema.sql diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..e62c9bd --- /dev/null +++ b/compose.yaml @@ -0,0 +1,45 @@ +services: + db: + image: postgres:16.1 + container_name: share-it + 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 + + db-init: + image: postgres:16.1 + container_name: db-init + depends_on: + db: + condition: service_healthy + entrypoint: + - bash + - -c + - | + set -e + psql postgresql://dbuser:12345@db:5432/shareit -v ON_ERROR_STOP=1 <<-EOSQL + DO \$\$ + DECLARE + r RECORD; + BEGIN + FOR r IN ( + SELECT schemaname, tablename + FROM pg_tables + WHERE schemaname = 'public' + ) LOOP + EXECUTE 'DROP TABLE IF EXISTS ' + || quote_ident(r.schemaname) || '.' + || quote_ident(r.tablename) || ' CASCADE'; + END LOOP; + END \$\$; + EOSQL diff --git a/pom.xml b/pom.xml index 1aef2f7..ea7d408 100644 --- a/pom.xml +++ b/pom.xml @@ -33,24 +33,6 @@ spring-boot-configuration-processor true - - - org.postgresql - postgresql - runtime - - - - org.projectlombok - lombok - true - - - - com.h2database - h2 - test - org.springframework.boot spring-boot-starter-test @@ -60,11 +42,19 @@ org.springframework.boot spring-boot-starter-validation - - org.zalando - logbook-spring-boot-starter - 3.9.0 + org.springframework.boot + spring-boot-starter-data-jpa + + + org.postgresql + postgresql + runtime + + + org.projectlombok + lombok + true diff --git a/src/main/java/ru/practicum/shareit/booking/Booking.java b/src/main/java/ru/practicum/shareit/booking/Booking.java index 4b63363..d40e4fa 100644 --- a/src/main/java/ru/practicum/shareit/booking/Booking.java +++ b/src/main/java/ru/practicum/shareit/booking/Booking.java @@ -1,7 +1,10 @@ package ru.practicum.shareit.booking; +import jakarta.persistence.*; import lombok.AccessLevel; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; import lombok.experimental.FieldDefaults; import ru.practicum.shareit.item.model.Item; import ru.practicum.shareit.user.User; @@ -11,13 +14,32 @@ /** * TODO Sprint add-bookings. */ +@Entity +@Table(name = "booking") @Data +@AllArgsConstructor +@NoArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) public class Booking { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) long id; + + @Column(name = "start_date", nullable = false) LocalDate start; + + @Column(name = "end_date", nullable = false) LocalDate end; + + @ManyToOne + @JoinColumn(name = "item_id", nullable = false) Item item; + + @ManyToOne + @JoinColumn(name = "booker_id", nullable = false) User booker; + + @Enumerated(EnumType.STRING) BookingStatus status; } 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..6236682 100644 --- a/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java +++ b/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java @@ -18,11 +18,11 @@ 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) 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..ade340f 100644 --- a/src/main/java/ru/practicum/shareit/item/model/Item.java +++ b/src/main/java/ru/practicum/shareit/item/model/Item.java @@ -1,9 +1,11 @@ package ru.practicum.shareit.item.model; +import jakarta.persistence.*; import jakarta.validation.constraints.NotNull; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; import lombok.experimental.FieldDefaults; import ru.practicum.shareit.request.ItemRequest; import ru.practicum.shareit.user.User; @@ -11,15 +13,33 @@ /** * TODO Sprint add-controllers. */ -@Data +@Entity +@Table(name = "items") @NotNull +@Data +@NoArgsConstructor @AllArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) 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 + @JoinColumn(name = "owner_id", nullable = false) User owner; + + @ManyToOne + @JoinColumn(name = "request_id") ItemRequest request; } diff --git a/src/main/java/ru/practicum/shareit/request/ItemRequest.java b/src/main/java/ru/practicum/shareit/request/ItemRequest.java index b6453d8..6789b6f 100644 --- a/src/main/java/ru/practicum/shareit/request/ItemRequest.java +++ b/src/main/java/ru/practicum/shareit/request/ItemRequest.java @@ -1,7 +1,10 @@ package ru.practicum.shareit.request; +import jakarta.persistence.*; import lombok.AccessLevel; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; import lombok.experimental.FieldDefaults; import ru.practicum.shareit.user.User; @@ -10,11 +13,24 @@ /** * TODO Sprint add-item-requests. */ +@Entity +@Table(name = "requests") @Data +@AllArgsConstructor +@NoArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) public class ItemRequest { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) long id; + String description; + + @ManyToOne + @JoinColumn(name = "requestor_id", nullable = false) User requestor; + + @Column(nullable = false) LocalDateTime created; } diff --git a/src/main/java/ru/practicum/shareit/user/User.java b/src/main/java/ru/practicum/shareit/user/User.java index bf9ffb2..fe13a2f 100644 --- a/src/main/java/ru/practicum/shareit/user/User.java +++ b/src/main/java/ru/practicum/shareit/user/User.java @@ -1,18 +1,24 @@ package ru.practicum.shareit.user; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Data; +import jakarta.persistence.*; +import lombok.*; import lombok.experimental.FieldDefaults; -/** - * TODO Sprint add-controllers. - */ +@Entity +@Table(name = "users") @Data +@NoArgsConstructor @AllArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) 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; } 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..559a3df 100644 --- a/src/main/java/ru/practicum/shareit/user/dto/UserDto.java +++ b/src/main/java/ru/practicum/shareit/user/dto/UserDto.java @@ -14,11 +14,11 @@ 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; diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 9d2c2c2..20fe7e3 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,18 +1,28 @@ -spring.jpa.hibernate.ddl-auto=none +server.port=8080 + +spring.jpa.hibernate.ddl-auto=validate +spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect spring.jpa.properties.hibernate.format_sql=true + spring.sql.init.mode=always +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 +hibernate.use-sql-comments=true + +logging.level.org.hibernate.SQL=DEBUG +logging.level.org.hibernate.orm.jdbc.bind=trace + 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.jackson.serialization.indent_output=true - -# TODO Append connection to Postgres DB -#spring.datasource.driverClassName -#spring.datasource.url -#spring.datasource.username -#spring.datasource.password diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql new file mode 100644 index 0000000..f11bcc0 --- /dev/null +++ b/src/main/resources/schema.sql @@ -0,0 +1,40 @@ +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), + CONSTRAINT UQ_USER_EMAIL UNIQUE (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(6) NOT NULL, + CONSTRAINT pk_requests PRIMARY KEY (id), + CONSTRAINT fk_requests_users_id FOREIGN KEY (requestor_id) REFERENCES public.users(id) +); + +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), + CONSTRAINT fk_items_requests_id FOREIGN KEY (request_id) REFERENCES public.requests(id) +); + +CREATE TABLE IF NOT EXISTS public.booking ( + id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL, + start_date date NOT NULL, + end_date date NOT NULL, + item_id bigint NOT NULL, + booker_id bigint NOT NULL, + status varchar(255) check (status in ('WAITING','APPROVED','REJECTED','CANCELED')), + CONSTRAINT pk_booking PRIMARY KEY (id), + CONSTRAINT fk_booking_items_id FOREIGN KEY (item_id) REFERENCES public.items(id), + CONSTRAINT fk_booking_users_id FOREIGN KEY (booker_id) REFERENCES public.users(id) + ); From f9aad3fda2e11e12fe6f33b4b5e72e2b5070238f Mon Sep 17 00:00:00 2001 From: AleksOBM Date: Thu, 26 Mar 2026 22:40:04 +0300 Subject: [PATCH 02/12] =?UTF-8?q?=D0=A2=D0=B5=D1=81=D1=82=D1=8B=20=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D0=B9=D0=B4=D0=B5=D0=BD=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 5 + .../shareit/booking/BookingController.java | 56 ++++- .../shareit/booking/BookingStatus.java | 8 - .../shareit/booking/dto/BookingDto.java | 54 +++++ .../shareit/booking/dto/BookingMapper.java | 36 ++++ .../shareit/booking/{ => model}/Booking.java | 14 +- .../shareit/booking/model/BookingStatus.java | 16 ++ .../shareit/booking/model/StateOfBooking.java | 36 ++++ .../booking/repository/BookingRepository.java | 95 +++++++++ .../booking/service/BookingService.java | 28 +++ .../booking/service/BookingServiceImpl.java | 200 ++++++++++++++++++ .../shareit/item/ItemController.java | 16 +- .../shareit/item/dto/CommentDto.java | 37 ++++ .../shareit/item/dto/ItemDtoWithComments.java | 36 ++++ .../shareit/item/dto/ItemDtoWithDates.java | 29 +++ .../shareit/item/dto/ItemMapper.java | 39 +++- .../practicum/shareit/item/model/Comment.java | 48 +++++ .../ru/practicum/shareit/item/model/Item.java | 5 +- .../item/repository/CommentRepository.java | 10 + .../item/repository/FakeItemRepository.java | 58 ----- .../item/repository/ItemRepository.java | 27 +-- .../shareit/item/service/ItemService.java | 21 +- .../shareit/item/service/ItemServiceImpl.java | 99 +++++++-- .../request/{ => model}/ItemRequest.java | 4 +- .../repository/FakeItemRequestRepository.java | 4 - .../repository/ItemRequestRepository.java | 5 +- .../shareit/user/dto/UserMapper.java | 2 +- .../shareit/user/{ => model}/User.java | 2 +- .../user/repository/FakeUserRepository.java | 62 ------ .../user/repository/UserRepository.java | 32 +-- .../shareit/user/service/UserService.java | 5 + .../shareit/user/service/UserServiceImpl.java | 12 +- .../shareit/util/IdentifyService.java | 20 -- .../ru/practicum/shareit/util/Marker.java | 4 - .../shareit/util/config/WebConfig.java | 1 - .../FutureOrPresentWithTolerance.java | 26 +++ ...FutureOrPresentWithToleranceValidator.java | 96 +++++++++ .../util/datetime/TimeRoundingStrategy.java | 9 + .../shareit/util/error/ErrorHandler.java | 18 +- .../exception/ConditionsNotMetException.java | 7 + .../util/exception/ForbiddenException.java | 7 + .../exception/ParameterNotValidException.java | 16 ++ src/main/resources/application.properties | 10 +- src/main/resources/schema.sql | 19 +- 44 files changed, 1071 insertions(+), 263 deletions(-) delete mode 100644 src/main/java/ru/practicum/shareit/booking/BookingStatus.java create mode 100644 src/main/java/ru/practicum/shareit/booking/dto/BookingMapper.java rename src/main/java/ru/practicum/shareit/booking/{ => model}/Booking.java (78%) create mode 100644 src/main/java/ru/practicum/shareit/booking/model/BookingStatus.java create mode 100644 src/main/java/ru/practicum/shareit/booking/model/StateOfBooking.java create mode 100644 src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java create mode 100644 src/main/java/ru/practicum/shareit/booking/service/BookingService.java create mode 100644 src/main/java/ru/practicum/shareit/booking/service/BookingServiceImpl.java create mode 100644 src/main/java/ru/practicum/shareit/item/dto/CommentDto.java create mode 100644 src/main/java/ru/practicum/shareit/item/dto/ItemDtoWithComments.java create mode 100644 src/main/java/ru/practicum/shareit/item/dto/ItemDtoWithDates.java create mode 100644 src/main/java/ru/practicum/shareit/item/model/Comment.java create mode 100644 src/main/java/ru/practicum/shareit/item/repository/CommentRepository.java delete mode 100644 src/main/java/ru/practicum/shareit/item/repository/FakeItemRepository.java rename src/main/java/ru/practicum/shareit/request/{ => model}/ItemRequest.java (87%) delete mode 100644 src/main/java/ru/practicum/shareit/request/repository/FakeItemRequestRepository.java rename src/main/java/ru/practicum/shareit/user/{ => model}/User.java (91%) delete mode 100644 src/main/java/ru/practicum/shareit/user/repository/FakeUserRepository.java delete mode 100644 src/main/java/ru/practicum/shareit/util/IdentifyService.java create mode 100644 src/main/java/ru/practicum/shareit/util/datetime/FutureOrPresentWithTolerance.java create mode 100644 src/main/java/ru/practicum/shareit/util/datetime/FutureOrPresentWithToleranceValidator.java create mode 100644 src/main/java/ru/practicum/shareit/util/datetime/TimeRoundingStrategy.java create mode 100644 src/main/java/ru/practicum/shareit/util/exception/ConditionsNotMetException.java create mode 100644 src/main/java/ru/practicum/shareit/util/exception/ForbiddenException.java create mode 100644 src/main/java/ru/practicum/shareit/util/exception/ParameterNotValidException.java diff --git a/pom.xml b/pom.xml index ea7d408..cb533d3 100644 --- a/pom.xml +++ b/pom.xml @@ -56,6 +56,11 @@ lombok true + + org.zalando + logbook-spring-boot-starter + 3.9.0 + diff --git a/src/main/java/ru/practicum/shareit/booking/BookingController.java b/src/main/java/ru/practicum/shareit/booking/BookingController.java index b94493d..629a782 100644 --- a/src/main/java/ru/practicum/shareit/booking/BookingController.java +++ b/src/main/java/ru/practicum/shareit/booking/BookingController.java @@ -1,12 +1,64 @@ 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.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 { + private 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 BookingDto 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..454176e 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,61 @@ package ru.practicum.shareit.booking.dto; +import jakarta.validation.constraints.Future; +import jakarta.validation.constraints.NotNull; +import lombok.AccessLevel; +import lombok.Data; +import lombok.experimental.FieldDefaults; +import ru.practicum.shareit.booking.model.BookingStatus; +import ru.practicum.shareit.item.dto.ItemDto; +import ru.practicum.shareit.user.dto.UserDto; +import ru.practicum.shareit.util.Marker; +import ru.practicum.shareit.util.datetime.FutureOrPresentWithTolerance; +import ru.practicum.shareit.util.datetime.TimeRoundingStrategy; + +import java.time.LocalDateTime; + /** * TODO Sprint add-bookings. */ +@Data +@NotNull +@FieldDefaults(level = AccessLevel.PRIVATE) public class BookingDto { + + Long id; + + @NotNull(groups = Marker.OnCreate.class, message = "Дата начала бронирования - обязательна") + @FutureOrPresentWithTolerance( + rounding = TimeRoundingStrategy.TRUNCATE_TO_SECONDS, + message = "Дата начала не может быть в прошлом" + ) + LocalDateTime start; + + @NotNull(groups = Marker.OnCreate.class, message = "Дата окончания бронирования - обязательна") + @Future(message = "Дата окончания должна быть в будущем") + LocalDateTime end; + + ItemDto item; + + UserDto booker; + + BookingStatus status; + + public BookingDto( + Long id, + LocalDateTime start, + LocalDateTime end, + Long itemId, + Long bookerId, + BookingStatus status + ) { + this.id = id != null && id <= 0 ? null : id; + this.start = start; + this.end = end; + this.item = itemId != null && itemId <= 0 ? null : + new ItemDto(itemId, null, null, null, null); + this.booker = bookerId != null && bookerId <= 0 ? null : + new UserDto(bookerId, null, null); + this.status = 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..c342afb --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/dto/BookingMapper.java @@ -0,0 +1,36 @@ +package ru.practicum.shareit.booking.dto; + +import ru.practicum.shareit.booking.model.Booking; +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 { + public static BookingDto toBookingDto(Booking booking) { + BookingDto bookingDto = new BookingDto( + booking.getId(), + booking.getStart().atZone(ZoneId.systemDefault()).toLocalDateTime(), + booking.getEnd().atZone(ZoneId.systemDefault()).toLocalDateTime(), + null, + null, + booking.getStatus() + ); + bookingDto.setItem(ItemMapper.toItemDto(booking.getItem())); + bookingDto.setBooker(UserMapper.toUserDto(booking.getBooker())); + return bookingDto; + } + + public static Booking toBooking(BookingDto bookingDto, Item item, User booker) { + return new Booking( + bookingDto.getId(), + bookingDto.getStart().atZone(ZoneId.systemDefault()).toLocalDateTime(), + bookingDto.getEnd().atZone(ZoneId.systemDefault()).toLocalDateTime(), + item, + booker, + bookingDto.getStatus() + ); + } +} diff --git a/src/main/java/ru/practicum/shareit/booking/Booking.java b/src/main/java/ru/practicum/shareit/booking/model/Booking.java similarity index 78% rename from src/main/java/ru/practicum/shareit/booking/Booking.java rename to src/main/java/ru/practicum/shareit/booking/model/Booking.java index d40e4fa..9223082 100644 --- a/src/main/java/ru/practicum/shareit/booking/Booking.java +++ b/src/main/java/ru/practicum/shareit/booking/model/Booking.java @@ -1,4 +1,4 @@ -package ru.practicum.shareit.booking; +package ru.practicum.shareit.booking.model; import jakarta.persistence.*; import lombok.AccessLevel; @@ -7,9 +7,9 @@ import lombok.NoArgsConstructor; import lombok.experimental.FieldDefaults; import ru.practicum.shareit.item.model.Item; -import ru.practicum.shareit.user.User; +import ru.practicum.shareit.user.model.User; -import java.time.LocalDate; +import java.time.LocalDateTime; /** * TODO Sprint add-bookings. @@ -24,19 +24,19 @@ public class Booking { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - long id; + Long id; @Column(name = "start_date", nullable = false) - LocalDate start; + LocalDateTime start; @Column(name = "end_date", nullable = false) - LocalDate end; + LocalDateTime end; @ManyToOne @JoinColumn(name = "item_id", nullable = false) Item item; - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "booker_id", nullable = false) User booker; 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..043c6af --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/model/BookingStatus.java @@ -0,0 +1,16 @@ +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..438f791 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/model/StateOfBooking.java @@ -0,0 +1,36 @@ +package ru.practicum.shareit.booking.model; + +import lombok.Getter; + +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); + } + + 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..78af99c --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java @@ -0,0 +1,95 @@ +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.util.List; + +public interface BookingRepository extends JpaRepository { + + @Query(""" + select b + from Booking b + left join Item i on i.id = b.item.id + where i.owner.id = :ownerId + order by b.start desc + """) + List findAllByOwner(Long ownerId); + + @Query(""" + select b + from Booking b + left join 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 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 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 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); + + List findAllByBookerIdOrderByStartDesc(Long bookerId); + + List findAllByBooker_IdAndStatusOrderByStartDesc(Long bookerId, BookingStatus status); + + List findBookingByItem_Id(Long itemId); +} 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..f97d11c --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/service/BookingService.java @@ -0,0 +1,28 @@ +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 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, BookingDto 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..843a239 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/service/BookingServiceImpl.java @@ -0,0 +1,200 @@ +package ru.practicum.shareit.booking.service; + +import jakarta.validation.ValidationException; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; +import lombok.experimental.FieldDefaults; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import ru.practicum.shareit.booking.dto.BookingDto; +import ru.practicum.shareit.booking.dto.BookingMapper; +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.ForbiddenException; +import ru.practicum.shareit.util.exception.NotFoundException; +import ru.practicum.shareit.util.exception.ParameterNotValidException; + +import java.time.LocalDateTime; +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) +public class BookingServiceImpl implements BookingService { + + BookingRepository bookingRepository; + ItemRepository itemRepository; + UserRepository userRepository; + + @Override + public BookingDto addNewBooking(Long userId, BookingDto bookingDto) { + LocalDateTime start = bookingDto.getStart(); + LocalDateTime end = bookingDto.getEnd(); + if (start.isAfter(end) || start.isEqual(end)) { + throw new ValidationException("Дата начала бронирования должна быть раньше даты окончания"); + } + if (bookingDto.getItem() == null) { + throw new ValidationException("item.id не может быть null"); + } + User booker = userRepository.findById(userId).orElseThrow( + () -> new ForbiddenException("Пользователь с id=" + userId + " не найден") + ); + Long itemId = bookingDto.getItem().getId(); + 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() + " не доступна для аренды"); + } + bookingDto.setStatus(BookingStatus.WAITING); + return BookingMapper.toBookingDto( + bookingRepository.save(BookingMapper.toBooking(bookingDto, item, booker)) + ); + } + + @Override + public BookingDto approveBooking(long userId, long bookingId, boolean approved) { + checkUser(userId); + Booking booking = getBooking(bookingId); + if (approved) { + booking.setStatus(BookingStatus.APPROVED); + } else { + booking.setStatus(BookingStatus.REJECTED); + } + 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 + " не найден."); + } + } + + 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..0ee4adf 100644 --- a/src/main/java/ru/practicum/shareit/item/ItemController.java +++ b/src/main/java/ru/practicum/shareit/item/ItemController.java @@ -4,7 +4,10 @@ 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.ItemDtoWithComments; +import ru.practicum.shareit.item.dto.ItemDtoWithDates; import ru.practicum.shareit.item.service.ItemService; import ru.practicum.shareit.util.Marker; @@ -22,12 +25,12 @@ public class ItemController { private final ItemService itemService; @GetMapping("/{itemId}") - public ItemDto getItem(@PathVariable long itemId) { + public ItemDtoWithComments getItem(@PathVariable long itemId) { return itemService.getItem(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 +51,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..bb43cf6 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/dto/CommentDto.java @@ -0,0 +1,37 @@ +package ru.practicum.shareit.item.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.NotBlank; +import lombok.Builder; +import lombok.Data; + +import ru.practicum.shareit.item.model.Comment; + +import java.time.LocalDateTime; + +@Data +@Builder +public class CommentDto { + + Long id; + + Long itemId; + + String authorName; + + @JsonProperty("created") + LocalDateTime createdDate; + + @NotBlank + String text; + + public static CommentDto from(Comment comment) { + return CommentDto.builder() + .id(comment.getId()) + .itemId(comment.getItem().getId()) + .authorName(comment.getAuthor().getName()) + .createdDate(comment.getCreatedDate()) + .text(comment.getText()) + .build(); + } +} diff --git a/src/main/java/ru/practicum/shareit/item/dto/ItemDtoWithComments.java b/src/main/java/ru/practicum/shareit/item/dto/ItemDtoWithComments.java new file mode 100644 index 0000000..17359ee --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/dto/ItemDtoWithComments.java @@ -0,0 +1,36 @@ +package ru.practicum.shareit.item.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.NotNull; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Data; +import lombok.experimental.FieldDefaults; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * TODO Sprint add-controllers. + */ +@Data +@NotNull +@Builder +@FieldDefaults(level = AccessLevel.PRIVATE) +public class ItemDtoWithComments { + 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/ItemDtoWithDates.java b/src/main/java/ru/practicum/shareit/item/dto/ItemDtoWithDates.java new file mode 100644 index 0000000..2d7f778 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/dto/ItemDtoWithDates.java @@ -0,0 +1,29 @@ +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; + +/** + * TODO Sprint add-controllers. + */ + +@Data +@NotNull +@Builder +@FieldDefaults(level = AccessLevel.PRIVATE) +public class ItemDtoWithDates { + Long id; + String name; + String description; + Boolean available; + + @JsonProperty("request") + Long requestId; + + LocalDateTime lastBookingDate; + LocalDateTime nextBookingDate; +} 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..159b16f 100644 --- a/src/main/java/ru/practicum/shareit/item/dto/ItemMapper.java +++ b/src/main/java/ru/practicum/shareit/item/dto/ItemMapper.java @@ -1,7 +1,10 @@ package ru.practicum.shareit.item.dto; 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) { @@ -14,6 +17,40 @@ public static ItemDto toItemDto(Item item) { ); } + public static ItemDtoWithDates toItemDtoWithDates( + Item item, + LocalDateTime lastBookingDate, + LocalDateTime nextBookingDate + ) { + return ItemDtoWithDates.builder() + .id(item.getId()) + .name(item.getName()) + .description(item.getDescription()) + .available(item.isAvailable()) + .requestId(item.getRequest() != null ? item.getRequest().getId() : null) + .lastBookingDate(lastBookingDate) + .nextBookingDate(nextBookingDate) + .build(); + } + + public static ItemDtoWithComments toItemDtoWithComments( + Item item, + LocalDateTime lastBookingDate, + LocalDateTime nextBookingDate, + List comments + ) { + return ItemDtoWithComments.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(), 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..edd57e0 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/model/Comment.java @@ -0,0 +1,48 @@ +package ru.practicum.shareit.item.model; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.FieldDefaults; +import ru.practicum.shareit.item.dto.CommentDto; +import ru.practicum.shareit.user.model.User; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "comments") +@Data +@NoArgsConstructor +@AllArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE) +public class Comment { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + Long id; + + @ManyToOne + @JoinColumn(name = "item_id") + Item item; + + @ManyToOne + @JoinColumn(name = "author_id") + User author; + + LocalDateTime createdDate; + + @Column(name = "comment_text") + String text; + + public static Comment from(CommentDto commentDto, Item item, User autor) { + return new Comment( + commentDto.getId(), + item, + autor, + commentDto.getCreatedDate(), + commentDto.getText() + ); + } +} 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 ade340f..c7868f4 100644 --- a/src/main/java/ru/practicum/shareit/item/model/Item.java +++ b/src/main/java/ru/practicum/shareit/item/model/Item.java @@ -7,8 +7,8 @@ import lombok.Data; import lombok.NoArgsConstructor; import lombok.experimental.FieldDefaults; -import ru.practicum.shareit.request.ItemRequest; -import ru.practicum.shareit.user.User; +import ru.practicum.shareit.request.model.ItemRequest; +import ru.practicum.shareit.user.model.User; /** * TODO Sprint add-controllers. @@ -32,6 +32,7 @@ public class Item { @Column String description; + /// статус о том, доступна или нет вещь для аренды @Column(nullable = false) boolean available; 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..fcb4f88 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/repository/CommentRepository.java @@ -0,0 +1,10 @@ +package ru.practicum.shareit.item.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import ru.practicum.shareit.item.model.Comment; + +import java.util.List; + +public interface CommentRepository extends JpaRepository { + List findAllByItemId(long itemId); +} 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..aead3a3 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,18 @@ 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 { + @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); - /// Получить вещь по id - Optional getOne(long itemId); - /// Получить все вещи - 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); } 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..068b346 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,39 @@ 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.ItemDtoWithComments; +import ru.practicum.shareit.item.dto.ItemDtoWithDates; import java.util.List; +@Transactional(readOnly = true) public interface ItemService { /// Получить вещь по id - ItemDto getItem(long itemId); + ItemDtoWithComments getItem(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..91b8d1d 100644 --- a/src/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java +++ b/src/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java @@ -4,14 +4,21 @@ import lombok.RequiredArgsConstructor; import lombok.experimental.FieldDefaults; 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.Duration; +import java.time.LocalDateTime; import java.util.Collections; import java.util.List; @@ -22,20 +29,32 @@ 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 ItemDtoWithComments getItem(long itemId) { + Item item = getItemWithCheckPresent(itemId); + return ItemMapper.toItemDtoWithComments( + item, + getLastBookingDate(item), + getNextBookingDate(item), + commentRepository.findAllByItemId(itemId).stream() + .map(CommentDto::from) + .toList() + ); } @Override - public List getAllItemsOfUser(long userId) { + public List getAllItemsOfUser(long userId) { checkUser(userId); - return itemRepository.findAll().stream() .filter(item -> item.getOwner().getId() == userId) - .map(ItemMapper::toItemDto) - .toList(); + .map(item -> ItemMapper.toItemDtoWithDates( + item, + getLastBookingDate(item), + getNextBookingDate(item) + )).toList(); } @Override @@ -43,12 +62,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(); } @@ -63,7 +77,6 @@ public ItemDto addNewItem(Long userId, ItemDto itemDto) { public ItemDto updateItem(long userId, long itemId, ItemDto itemDto) { checkUser(userId); Item oldItem = getItemWithCheckPresentAndOwner(itemId, userId); - Item item = new Item( itemId, itemDto.getName() == null ? oldItem.getName() : itemDto.getName(), @@ -72,30 +85,55 @@ public ItemDto updateItem(long userId, long itemId, ItemDto itemDto) { getUserWithCheckPresent(userId), null ); - return ItemMapper.toItemDto(itemRepository.update(itemId, item)); + 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, CommentDto commentDto) { + String text = commentDto.getText(); + if (text == null || text.isBlank()) { + throw new ParameterNotValidException("text", "Текст комментария не может быть пустым"); + } + User user = getUserWithCheckPresent(userId); + Item item = getItemWithCheckPresent(itemId); + if (item.getOwner().getId().equals(userId)) { + throw new ForbiddenException("Нельзя оставить комментарий к своей вещи"); + } + if (bookingRepository.findBookingByItem_Id(item.getId()).stream() + .noneMatch(booking -> booking.getBooker().getId().equals(userId) && + booking.getEnd().isBefore(LocalDateTime.now())) + ) { + throw new ParameterNotValidException( + "userId", "Комментарии доступны только тем, кто уже по пользовался вешью" + ); + } + return CommentDto.from(commentRepository.save( + new Comment(null, item, user, LocalDateTime.now(), text.trim()) + ) + ); } private void checkUser(long userId) { - if (userRepository.checkUserIsNotPresent(userId)) { + if (!userRepository.existsById(userId)) { throw new NotFoundException("Пользователь с id=" + userId + " не найден."); } } private User getUserWithCheckPresent(long userId) { - return userRepository.findOne(userId).orElseThrow(() -> + return userRepository.findById(userId).orElseThrow(() -> new NotFoundException("Пользователь с id=" + userId + " не найден.") ); } private Item getItemWithCheckPresent(long itemId) { - return itemRepository.getOne(itemId).orElseThrow(() -> + return itemRepository.findById(itemId).orElseThrow(() -> new NotFoundException("Вещь с id=" + itemId + " не найдена.") ); } @@ -107,4 +145,23 @@ private Item getItemWithCheckPresentAndOwner(long itemId, long userId) { } throw new NotFoundException("Пользователь с id=" + userId + " не является владельцем вещи с id=" + itemId); } + + private LocalDateTime getLastBookingDate(Item item) { + return bookingRepository.findBookingByItem_Id(item.getId()).stream() + .filter(booking -> Duration.between(booking.getStart(), booking.getEnd()).toMinutes() > 30) + .map(Booking::getStart) + .filter(ldt -> ldt.isBefore(LocalDateTime.now()) || + ldt.isEqual(LocalDateTime.now())) + .sorted() + .findFirst().orElse(null); + } + + private LocalDateTime getNextBookingDate(Item item) { + return bookingRepository.findBookingByItem_Id(item.getId()).stream() + .filter(booking -> Duration.between(booking.getStart(), booking.getEnd()).toMinutes() > 30) + .map(Booking::getStart) + .filter(ldt -> ldt.isAfter(LocalDateTime.now())) + .sorted() + .findFirst().orElse(null); + } } diff --git a/src/main/java/ru/practicum/shareit/request/ItemRequest.java b/src/main/java/ru/practicum/shareit/request/model/ItemRequest.java similarity index 87% rename from src/main/java/ru/practicum/shareit/request/ItemRequest.java rename to src/main/java/ru/practicum/shareit/request/model/ItemRequest.java index 6789b6f..ec18210 100644 --- a/src/main/java/ru/practicum/shareit/request/ItemRequest.java +++ b/src/main/java/ru/practicum/shareit/request/model/ItemRequest.java @@ -1,4 +1,4 @@ -package ru.practicum.shareit.request; +package ru.practicum.shareit.request.model; import jakarta.persistence.*; import lombok.AccessLevel; @@ -6,7 +6,7 @@ import lombok.Data; import lombok.NoArgsConstructor; import lombok.experimental.FieldDefaults; -import ru.practicum.shareit.user.User; +import ru.practicum.shareit.user.model.User; import java.time.LocalDateTime; 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/dto/UserMapper.java b/src/main/java/ru/practicum/shareit/user/dto/UserMapper.java index 2e0ca25..79b7cd1 100644 --- a/src/main/java/ru/practicum/shareit/user/dto/UserMapper.java +++ b/src/main/java/ru/practicum/shareit/user/dto/UserMapper.java @@ -1,6 +1,6 @@ package ru.practicum.shareit.user.dto; -import ru.practicum.shareit.user.User; +import ru.practicum.shareit.user.model.User; public class UserMapper { diff --git a/src/main/java/ru/practicum/shareit/user/User.java b/src/main/java/ru/practicum/shareit/user/model/User.java similarity index 91% rename from src/main/java/ru/practicum/shareit/user/User.java rename to src/main/java/ru/practicum/shareit/user/model/User.java index fe13a2f..db579bc 100644 --- a/src/main/java/ru/practicum/shareit/user/User.java +++ b/src/main/java/ru/practicum/shareit/user/model/User.java @@ -1,4 +1,4 @@ -package ru.practicum.shareit.user; +package ru.practicum.shareit.user.model; import jakarta.persistence.*; import lombok.*; 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..620e563 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,10 @@ package ru.practicum.shareit.user.repository; -import ru.practicum.shareit.user.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import ru.practicum.shareit.user.model.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); +@Repository +public interface UserRepository extends JpaRepository { + boolean existsByEmail(String email); } 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..c2e08f5 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,26 @@ 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 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..d4c0644 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; @@ -45,25 +45,25 @@ 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); + userRepository.deleteById(userId); } 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 index ef0380a..e3527fc 100644 --- a/src/main/java/ru/practicum/shareit/util/config/WebConfig.java +++ b/src/main/java/ru/practicum/shareit/util/config/WebConfig.java @@ -4,5 +4,4 @@ @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..0cd746b --- /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 { + + private long toleranceMillis; + private String zoneIdStr; + private 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..7852307 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,20 @@ 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.CONFLICT) @ExceptionHandler public ErrorResponse handleDuplicatedData(final DuplicatedDataException e) { 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/resources/application.properties b/src/main/resources/application.properties index 20fe7e3..ec063b7 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,4 +1,5 @@ server.port=8080 +spring.main.log-startup-info=false spring.jpa.hibernate.ddl-auto=validate spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect @@ -17,12 +18,11 @@ hibernate.jdbc.time-zone=UTC hibernate.default-schema=public hibernate.show-sql=true hibernate.format-sql=true -hibernate.use-sql-comments=true + +logging.level.root=error +logging.level.web=info logging.level.org.hibernate.SQL=DEBUG logging.level.org.hibernate.orm.jdbc.bind=trace -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.zalando.logbook=TRACE diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index f11bcc0..477876d 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -3,14 +3,14 @@ CREATE TABLE IF NOT EXISTS public.users ( user_name VARCHAR(255) NOT NULL, email VARCHAR(512) NOT NULL, CONSTRAINT pk_users PRIMARY KEY (id), - CONSTRAINT UQ_USER_EMAIL UNIQUE (email) + CONSTRAINT uq_user_email UNIQUE (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(6) 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) ); @@ -29,8 +29,8 @@ CREATE TABLE IF NOT EXISTS public.items ( CREATE TABLE IF NOT EXISTS public.booking ( id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL, - start_date date NOT NULL, - end_date date 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')), @@ -38,3 +38,14 @@ CREATE TABLE IF NOT EXISTS public.booking ( CONSTRAINT fk_booking_items_id FOREIGN KEY (item_id) REFERENCES public.items(id), CONSTRAINT fk_booking_users_id FOREIGN KEY (booker_id) REFERENCES public.users(id) ); + +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), + CONSTRAINT fk_comments_users_id FOREIGN KEY (item_id) REFERENCES public.users(id) +); From ed7dc1b85b6381be6c724fe308c819529f859a92 Mon Sep 17 00:00:00 2001 From: AleksOBM Date: Fri, 27 Mar 2026 04:33:55 +0300 Subject: [PATCH 03/12] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B2=D0=BE=D0=B5=20?= =?UTF-8?q?=D1=80=D0=B5=D0=B2=D1=8C=D1=8E=2015-=D0=B3=D0=BE=20=D1=81=D0=BF?= =?UTF-8?q?=D1=80=D0=B8=D0=BD=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../shareit/booking/dto/BookingMapper.java | 8 +++- .../shareit/booking/model/Booking.java | 2 +- .../shareit/booking/model/BookingStatus.java | 1 + .../shareit/booking/model/StateOfBooking.java | 3 ++ .../booking/repository/BookingRepository.java | 15 ++++--- .../booking/service/BookingServiceImpl.java | 6 +-- .../shareit/item/dto/CommentDto.java | 3 +- .../shareit/item/dto/ItemMapper.java | 12 ++++-- .../practicum/shareit/item/model/Comment.java | 15 +------ .../ru/practicum/shareit/item/model/Item.java | 4 +- .../item/repository/ItemRepository.java | 3 +- .../shareit/item/service/ItemServiceImpl.java | 39 ++++++++++++------- .../shareit/request/model/ItemRequest.java | 2 +- .../user/repository/UserRepository.java | 6 +++ .../shareit/user/service/UserService.java | 1 + .../shareit/user/service/UserServiceImpl.java | 4 +- src/main/resources/schema.sql | 14 +++---- 17 files changed, 83 insertions(+), 55 deletions(-) diff --git a/src/main/java/ru/practicum/shareit/booking/dto/BookingMapper.java b/src/main/java/ru/practicum/shareit/booking/dto/BookingMapper.java index c342afb..e707763 100644 --- a/src/main/java/ru/practicum/shareit/booking/dto/BookingMapper.java +++ b/src/main/java/ru/practicum/shareit/booking/dto/BookingMapper.java @@ -1,5 +1,6 @@ package ru.practicum.shareit.booking.dto; +import org.springframework.lang.NonNull; import ru.practicum.shareit.booking.model.Booking; import ru.practicum.shareit.item.dto.ItemMapper; import ru.practicum.shareit.item.model.Item; @@ -9,7 +10,9 @@ import java.time.ZoneId; public class BookingMapper { - public static BookingDto toBookingDto(Booking booking) { + + @NonNull + public static BookingDto toBookingDto(@NonNull Booking booking) { BookingDto bookingDto = new BookingDto( booking.getId(), booking.getStart().atZone(ZoneId.systemDefault()).toLocalDateTime(), @@ -23,7 +26,8 @@ public static BookingDto toBookingDto(Booking booking) { return bookingDto; } - public static Booking toBooking(BookingDto bookingDto, Item item, User booker) { + @NonNull + public static Booking toBooking(@NonNull BookingDto bookingDto, Item item, User booker) { return new Booking( bookingDto.getId(), bookingDto.getStart().atZone(ZoneId.systemDefault()).toLocalDateTime(), diff --git a/src/main/java/ru/practicum/shareit/booking/model/Booking.java b/src/main/java/ru/practicum/shareit/booking/model/Booking.java index 9223082..d807fea 100644 --- a/src/main/java/ru/practicum/shareit/booking/model/Booking.java +++ b/src/main/java/ru/practicum/shareit/booking/model/Booking.java @@ -32,7 +32,7 @@ public class Booking { @Column(name = "end_date", nullable = false) LocalDateTime end; - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "item_id", nullable = false) Item item; diff --git a/src/main/java/ru/practicum/shareit/booking/model/BookingStatus.java b/src/main/java/ru/practicum/shareit/booking/model/BookingStatus.java index 043c6af..a5b6420 100644 --- a/src/main/java/ru/practicum/shareit/booking/model/BookingStatus.java +++ b/src/main/java/ru/practicum/shareit/booking/model/BookingStatus.java @@ -1,5 +1,6 @@ package ru.practicum.shareit.booking.model; +/// Статус бронирования вещи public enum BookingStatus { /// новое бронирование, ожидает одобрения diff --git a/src/main/java/ru/practicum/shareit/booking/model/StateOfBooking.java b/src/main/java/ru/practicum/shareit/booking/model/StateOfBooking.java index 438f791..0190228 100644 --- a/src/main/java/ru/practicum/shareit/booking/model/StateOfBooking.java +++ b/src/main/java/ru/practicum/shareit/booking/model/StateOfBooking.java @@ -1,10 +1,12 @@ 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; @@ -28,6 +30,7 @@ public static StateOfBooking of(String text) { return StateOfBooking.valueOf(state); } + @NonNull public static List getValidValues() { return Arrays.stream(values()).filter(v -> v.ordinal() > 0) .map(StateOfBooking::name) diff --git a/src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java b/src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java index 78af99c..77a4374 100644 --- a/src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java +++ b/src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java @@ -6,13 +6,14 @@ import ru.practicum.shareit.booking.model.BookingStatus; import java.util.List; +import java.util.Set; public interface BookingRepository extends JpaRepository { @Query(""" select b from Booking b - left join Item i on i.id = b.item.id + left join fetch Item i on i.id = b.item.id where i.owner.id = :ownerId order by b.start desc """) @@ -21,7 +22,7 @@ public interface BookingRepository extends JpaRepository { @Query(""" select b from Booking b - left join Item i on i.id = b.item.id + 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 @@ -32,7 +33,7 @@ public interface BookingRepository extends JpaRepository { @Query(""" select b from Booking b - left join Item i on i.id = b.item.id + 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 @@ -42,7 +43,7 @@ public interface BookingRepository extends JpaRepository { @Query(""" select b from Booking b - left join Item i on i.id = b.item.id + 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 @@ -52,7 +53,7 @@ public interface BookingRepository extends JpaRepository { @Query(""" select b from Booking b - left join Item i on i.id = b.item.id + 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 @@ -91,5 +92,7 @@ public interface BookingRepository extends JpaRepository { List findAllByBooker_IdAndStatusOrderByStartDesc(Long bookerId, BookingStatus status); - List findBookingByItem_Id(Long itemId); + List findAllBookingByItem_Id(Long itemId); + + List findAllBookingByItemIdIn(Set itemIds); } diff --git a/src/main/java/ru/practicum/shareit/booking/service/BookingServiceImpl.java b/src/main/java/ru/practicum/shareit/booking/service/BookingServiceImpl.java index 843a239..e0559c5 100644 --- a/src/main/java/ru/practicum/shareit/booking/service/BookingServiceImpl.java +++ b/src/main/java/ru/practicum/shareit/booking/service/BookingServiceImpl.java @@ -4,7 +4,7 @@ import lombok.AccessLevel; import lombok.RequiredArgsConstructor; import lombok.experimental.FieldDefaults; -import lombok.extern.slf4j.Slf4j; +import org.springframework.lang.NonNull; import org.springframework.stereotype.Service; import ru.practicum.shareit.booking.dto.BookingDto; import ru.practicum.shareit.booking.dto.BookingMapper; @@ -23,7 +23,6 @@ import java.time.LocalDateTime; import java.util.List; -@Slf4j @Service @RequiredArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) @@ -34,7 +33,7 @@ public class BookingServiceImpl implements BookingService { UserRepository userRepository; @Override - public BookingDto addNewBooking(Long userId, BookingDto bookingDto) { + public BookingDto addNewBooking(Long userId, @NonNull BookingDto bookingDto) { LocalDateTime start = bookingDto.getStart(); LocalDateTime end = bookingDto.getEnd(); if (start.isAfter(end) || start.isEqual(end)) { @@ -192,6 +191,7 @@ private void checkUser(Long 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/dto/CommentDto.java b/src/main/java/ru/practicum/shareit/item/dto/CommentDto.java index bb43cf6..958cc85 100644 --- a/src/main/java/ru/practicum/shareit/item/dto/CommentDto.java +++ b/src/main/java/ru/practicum/shareit/item/dto/CommentDto.java @@ -5,6 +5,7 @@ import lombok.Builder; import lombok.Data; +import org.springframework.lang.NonNull; import ru.practicum.shareit.item.model.Comment; import java.time.LocalDateTime; @@ -25,7 +26,7 @@ public class CommentDto { @NotBlank String text; - public static CommentDto from(Comment comment) { + public static CommentDto from(@NonNull Comment comment) { return CommentDto.builder() .id(comment.getId()) .itemId(comment.getItem().getId()) 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 159b16f..931126f 100644 --- a/src/main/java/ru/practicum/shareit/item/dto/ItemMapper.java +++ b/src/main/java/ru/practicum/shareit/item/dto/ItemMapper.java @@ -1,5 +1,6 @@ package ru.practicum.shareit.item.dto; +import org.springframework.lang.NonNull; import ru.practicum.shareit.item.model.Item; import ru.practicum.shareit.user.model.User; @@ -7,7 +8,9 @@ import java.util.List; public class ItemMapper { - public static ItemDto toItemDto(Item item) { + + @NonNull + public static ItemDto toItemDto(@NonNull Item item) { return new ItemDto( item.getId(), item.getName(), @@ -18,7 +21,7 @@ public static ItemDto toItemDto(Item item) { } public static ItemDtoWithDates toItemDtoWithDates( - Item item, + @NonNull Item item, LocalDateTime lastBookingDate, LocalDateTime nextBookingDate ) { @@ -34,7 +37,7 @@ public static ItemDtoWithDates toItemDtoWithDates( } public static ItemDtoWithComments toItemDtoWithComments( - Item item, + @NonNull Item item, LocalDateTime lastBookingDate, LocalDateTime nextBookingDate, List comments @@ -51,7 +54,8 @@ public static ItemDtoWithComments toItemDtoWithComments( .build(); } - public static Item toItem(ItemDto itemDto, User user) { + @NonNull + public static Item toItem(@NonNull ItemDto itemDto, User user) { return new Item( itemDto.getId(), itemDto.getName(), diff --git a/src/main/java/ru/practicum/shareit/item/model/Comment.java b/src/main/java/ru/practicum/shareit/item/model/Comment.java index edd57e0..9bac980 100644 --- a/src/main/java/ru/practicum/shareit/item/model/Comment.java +++ b/src/main/java/ru/practicum/shareit/item/model/Comment.java @@ -6,7 +6,6 @@ import lombok.Data; import lombok.NoArgsConstructor; import lombok.experimental.FieldDefaults; -import ru.practicum.shareit.item.dto.CommentDto; import ru.practicum.shareit.user.model.User; import java.time.LocalDateTime; @@ -23,11 +22,11 @@ public class Comment { @GeneratedValue(strategy = GenerationType.IDENTITY) Long id; - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "item_id") Item item; - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "author_id") User author; @@ -35,14 +34,4 @@ public class Comment { @Column(name = "comment_text") String text; - - public static Comment from(CommentDto commentDto, Item item, User autor) { - return new Comment( - commentDto.getId(), - item, - autor, - commentDto.getCreatedDate(), - commentDto.getText() - ); - } } 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 c7868f4..be07991 100644 --- a/src/main/java/ru/practicum/shareit/item/model/Item.java +++ b/src/main/java/ru/practicum/shareit/item/model/Item.java @@ -36,11 +36,11 @@ public class Item { @Column(nullable = false) boolean available; - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "owner_id", nullable = false) User owner; - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "request_id") 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 index aead3a3..7f1fe38 100644 --- a/src/main/java/ru/practicum/shareit/item/repository/ItemRepository.java +++ b/src/main/java/ru/practicum/shareit/item/repository/ItemRepository.java @@ -7,6 +7,7 @@ import java.util.List; public interface ItemRepository extends JpaRepository { + @Query(" select i from Item i " + "where (upper(i.name) like upper(concat('%', ?1, '%')) " + " or upper(i.description) like upper(concat('%', ?1, '%'))) " + @@ -14,5 +15,5 @@ public interface ItemRepository extends JpaRepository { ) List search(String text); - + List findAllByOwner_Id(Long ownerId); } 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 91b8d1d..3228f21 100644 --- a/src/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java +++ b/src/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java @@ -3,6 +3,7 @@ import lombok.AccessLevel; import lombok.RequiredArgsConstructor; import lombok.experimental.FieldDefaults; +import org.springframework.lang.NonNull; import org.springframework.stereotype.Service; import ru.practicum.shareit.booking.model.Booking; import ru.practicum.shareit.booking.repository.BookingRepository; @@ -21,6 +22,9 @@ import java.time.LocalDateTime; 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) @@ -35,10 +39,12 @@ public class ItemServiceImpl implements ItemService { @Override public ItemDtoWithComments getItem(long itemId) { Item item = getItemWithCheckPresent(itemId); + List bookings = bookingRepository.findAllBookingByItem_Id(item.getId()); + return ItemMapper.toItemDtoWithComments( item, - getLastBookingDate(item), - getNextBookingDate(item), + getLastBookingDate(bookings), + getNextBookingDate(bookings), commentRepository.findAllByItemId(itemId).stream() .map(CommentDto::from) .toList() @@ -48,12 +54,16 @@ public ItemDtoWithComments getItem(long itemId) { @Override public List getAllItemsOfUser(long userId) { checkUser(userId); - return itemRepository.findAll().stream() - .filter(item -> item.getOwner().getId() == userId) + Map items = itemRepository.findAllByOwner_Id(userId) + .stream() + .collect(Collectors.toMap(Item::getId, Function.identity())); + List bookings = bookingRepository.findAllBookingByItemIdIn(items.keySet()); + + return items.values().stream() .map(item -> ItemMapper.toItemDtoWithDates( item, - getLastBookingDate(item), - getNextBookingDate(item) + getLastBookingDate(bookings), + getNextBookingDate(bookings) )).toList(); } @@ -74,7 +84,7 @@ 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( @@ -96,7 +106,7 @@ public void deleteItem(long userId, long itemId) { } @Override - public CommentDto addComment(long userId, long itemId, CommentDto commentDto) { + public CommentDto addComment(long userId, long itemId, @NonNull CommentDto commentDto) { String text = commentDto.getText(); if (text == null || text.isBlank()) { throw new ParameterNotValidException("text", "Текст комментария не может быть пустым"); @@ -106,7 +116,7 @@ public CommentDto addComment(long userId, long itemId, CommentDto commentDto) { if (item.getOwner().getId().equals(userId)) { throw new ForbiddenException("Нельзя оставить комментарий к своей вещи"); } - if (bookingRepository.findBookingByItem_Id(item.getId()).stream() + if (bookingRepository.findAllBookingByItem_Id(item.getId()).stream() .noneMatch(booking -> booking.getBooker().getId().equals(userId) && booking.getEnd().isBefore(LocalDateTime.now())) ) { @@ -126,18 +136,21 @@ private void checkUser(long userId) { } } + @NonNull private User getUserWithCheckPresent(long userId) { return userRepository.findById(userId).orElseThrow(() -> new NotFoundException("Пользователь с id=" + userId + " не найден.") ); } + @NonNull private Item getItemWithCheckPresent(long itemId) { 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) { @@ -146,8 +159,8 @@ private Item getItemWithCheckPresentAndOwner(long itemId, long userId) { throw new NotFoundException("Пользователь с id=" + userId + " не является владельцем вещи с id=" + itemId); } - private LocalDateTime getLastBookingDate(Item item) { - return bookingRepository.findBookingByItem_Id(item.getId()).stream() + private LocalDateTime getLastBookingDate(@NonNull List bookings) { + return bookings.stream() .filter(booking -> Duration.between(booking.getStart(), booking.getEnd()).toMinutes() > 30) .map(Booking::getStart) .filter(ldt -> ldt.isBefore(LocalDateTime.now()) || @@ -156,8 +169,8 @@ private LocalDateTime getLastBookingDate(Item item) { .findFirst().orElse(null); } - private LocalDateTime getNextBookingDate(Item item) { - return bookingRepository.findBookingByItem_Id(item.getId()).stream() + private LocalDateTime getNextBookingDate(@NonNull List bookings) { + return bookings.stream() .filter(booking -> Duration.between(booking.getStart(), booking.getEnd()).toMinutes() > 30) .map(Booking::getStart) .filter(ldt -> ldt.isAfter(LocalDateTime.now())) diff --git a/src/main/java/ru/practicum/shareit/request/model/ItemRequest.java b/src/main/java/ru/practicum/shareit/request/model/ItemRequest.java index ec18210..e1235f1 100644 --- a/src/main/java/ru/practicum/shareit/request/model/ItemRequest.java +++ b/src/main/java/ru/practicum/shareit/request/model/ItemRequest.java @@ -27,7 +27,7 @@ public class ItemRequest { String description; - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "requestor_id", nullable = false) User requestor; 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 620e563..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,10 +1,16 @@ package ru.practicum.shareit.user.repository; 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 c2e08f5..45ad6ad 100644 --- a/src/main/java/ru/practicum/shareit/user/service/UserService.java +++ b/src/main/java/ru/practicum/shareit/user/service/UserService.java @@ -17,6 +17,7 @@ public interface UserService { UserDto saveUser(UserDto userDto); /// Удалить пользователя по id + @Transactional void deleteUser(long userId); /// Обновить данные пользователя по 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 d4c0644..b0f82aa 100644 --- a/src/main/java/ru/practicum/shareit/user/service/UserServiceImpl.java +++ b/src/main/java/ru/practicum/shareit/user/service/UserServiceImpl.java @@ -53,7 +53,9 @@ public void deleteUser(long userId) { if (!userRepository.existsById(userId)) { throw new NotFoundException("Пользователь с id=" + userId + " не найден."); } - userRepository.deleteById(userId); + if (userRepository.deleteUserById(userId) < 1) { + throw new RuntimeException("Удаление не удалось"); + } } private void checkEmail(String email) { diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 477876d..3fca937 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -12,7 +12,7 @@ CREATE TABLE IF NOT EXISTS public.requests ( 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) + CONSTRAINT fk_requests_users_id FOREIGN KEY (requestor_id) REFERENCES public.users(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS public.items ( @@ -23,8 +23,8 @@ CREATE TABLE IF NOT EXISTS public.items ( 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), - CONSTRAINT fk_items_requests_id FOREIGN KEY (request_id) REFERENCES public.requests(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 TABLE IF NOT EXISTS public.booking ( @@ -35,8 +35,8 @@ CREATE TABLE IF NOT EXISTS public.booking ( booker_id bigint NOT NULL, status varchar(255) check (status in ('WAITING','APPROVED','REJECTED','CANCELED')), CONSTRAINT pk_booking PRIMARY KEY (id), - CONSTRAINT fk_booking_items_id FOREIGN KEY (item_id) REFERENCES public.items(id), - CONSTRAINT fk_booking_users_id FOREIGN KEY (booker_id) REFERENCES public.users(id) + CONSTRAINT fk_booking_items_id FOREIGN KEY (item_id) REFERENCES public.items(id) ON DELETE CASCADE, + CONSTRAINT fk_booking_users_id FOREIGN KEY (booker_id) REFERENCES public.users(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS public.comments ( @@ -46,6 +46,6 @@ CREATE TABLE IF NOT EXISTS public.comments ( 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), - CONSTRAINT fk_comments_users_id FOREIGN KEY (item_id) REFERENCES public.users(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 ); From c87772bf9549db08d4c3ad5ee556285a220f2497 Mon Sep 17 00:00:00 2001 From: AleksOBM Date: Fri, 27 Mar 2026 04:45:30 +0300 Subject: [PATCH 04/12] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B2=D0=BE=D0=B5=20?= =?UTF-8?q?=D1=80=D0=B5=D0=B2=D1=8C=D1=8E=2015-=D0=B3=D0=BE=20=D1=81=D0=BF?= =?UTF-8?q?=D1=80=D0=B8=D0=BD=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Попытка пофиксить Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:3.2.5:test --- pom.xml | 5 ----- .../java/ru/practicum/shareit/util/config/WebConfig.java | 7 ------- src/main/resources/application.properties | 2 -- 3 files changed, 14 deletions(-) delete mode 100644 src/main/java/ru/practicum/shareit/util/config/WebConfig.java diff --git a/pom.xml b/pom.xml index cb533d3..ea7d408 100644 --- a/pom.xml +++ b/pom.xml @@ -56,11 +56,6 @@ lombok true - - org.zalando - logbook-spring-boot-starter - 3.9.0 - 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 e3527fc..0000000 --- a/src/main/java/ru/practicum/shareit/util/config/WebConfig.java +++ /dev/null @@ -1,7 +0,0 @@ -package ru.practicum.shareit.util.config; - -import org.springframework.context.annotation.Configuration; - -@Configuration -public class WebConfig { -} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index ec063b7..3bd5fb7 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -24,5 +24,3 @@ logging.level.web=info logging.level.org.hibernate.SQL=DEBUG logging.level.org.hibernate.orm.jdbc.bind=trace - -logging.level.org.zalando.logbook=TRACE From 5931b0fe2080856bb4845498d32871f1fd0119e9 Mon Sep 17 00:00:00 2001 From: AleksOBM Date: Fri, 27 Mar 2026 05:02:09 +0300 Subject: [PATCH 05/12] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B2=D0=BE=D0=B5=20?= =?UTF-8?q?=D1=80=D0=B5=D0=B2=D1=8C=D1=8E=2015-=D0=B3=D0=BE=20=D1=81=D0=BF?= =?UTF-8?q?=D1=80=D0=B8=D0=BD=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Попытка пофиксить Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:3.2.5:test --- src/main/resources/application.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 3bd5fb7..8bb5bc6 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,7 +1,7 @@ server.port=8080 spring.main.log-startup-info=false -spring.jpa.hibernate.ddl-auto=validate +spring.jpa.hibernate.ddl-auto=none spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect spring.jpa.properties.hibernate.format_sql=true From 73acfdde8100eeca52f0e5b49c14bae811a6bfdf Mon Sep 17 00:00:00 2001 From: AleksOBM Date: Fri, 27 Mar 2026 05:09:14 +0300 Subject: [PATCH 06/12] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B2=D0=BE=D0=B5=20?= =?UTF-8?q?=D1=80=D0=B5=D0=B2=D1=8C=D1=8E=2015-=D0=B3=D0=BE=20=D1=81=D0=BF?= =?UTF-8?q?=D1=80=D0=B8=D0=BD=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Попытка 3 пофиксить Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:3.2.5:test --- compose.yaml | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/compose.yaml b/compose.yaml index e62c9bd..ae1e654 100644 --- a/compose.yaml +++ b/compose.yaml @@ -1,7 +1,6 @@ services: db: image: postgres:16.1 - container_name: share-it ports: - "5432:5432" volumes: @@ -15,31 +14,3 @@ services: timeout: 5s interval: 5s retries: 10 - - db-init: - image: postgres:16.1 - container_name: db-init - depends_on: - db: - condition: service_healthy - entrypoint: - - bash - - -c - - | - set -e - psql postgresql://dbuser:12345@db:5432/shareit -v ON_ERROR_STOP=1 <<-EOSQL - DO \$\$ - DECLARE - r RECORD; - BEGIN - FOR r IN ( - SELECT schemaname, tablename - FROM pg_tables - WHERE schemaname = 'public' - ) LOOP - EXECUTE 'DROP TABLE IF EXISTS ' - || quote_ident(r.schemaname) || '.' - || quote_ident(r.tablename) || ' CASCADE'; - END LOOP; - END \$\$; - EOSQL From cdc5aba35065d48d3e5906965f72b266c9d525bd Mon Sep 17 00:00:00 2001 From: AleksOBM Date: Fri, 27 Mar 2026 05:21:42 +0300 Subject: [PATCH 07/12] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B2=D0=BE=D0=B5=20?= =?UTF-8?q?=D1=80=D0=B5=D0=B2=D1=8C=D1=8E=2015-=D0=B3=D0=BE=20=D1=81=D0=BF?= =?UTF-8?q?=D1=80=D0=B8=D0=BD=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Попытка 4 пофиксить Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:3.2.5:test --- pom.xml | 1 - src/main/resources/application.properties | 12 ------------ 2 files changed, 13 deletions(-) diff --git a/pom.xml b/pom.xml index ea7d408..5a72606 100644 --- a/pom.xml +++ b/pom.xml @@ -49,7 +49,6 @@ org.postgresql postgresql - runtime org.projectlombok diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 8bb5bc6..a97dc27 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,26 +1,14 @@ -server.port=8080 spring.main.log-startup-info=false - spring.jpa.hibernate.ddl-auto=none -spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.properties.hibernate.format_sql=true - -spring.sql.init.mode=always 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 logging.level.root=error -logging.level.web=info - -logging.level.org.hibernate.SQL=DEBUG -logging.level.org.hibernate.orm.jdbc.bind=trace From 46ab38e844ffffa6a58ac7c96176a5a6533dfa2c Mon Sep 17 00:00:00 2001 From: AleksOBM Date: Fri, 27 Mar 2026 06:24:50 +0300 Subject: [PATCH 08/12] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B2=D0=BE=D0=B5=20?= =?UTF-8?q?=D1=80=D0=B5=D0=B2=D1=8C=D1=8E=2015-=D0=B3=D0=BE=20=D1=81=D0=BF?= =?UTF-8?q?=D1=80=D0=B8=D0=BD=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Попытка 5 пофиксить Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:3.2.5:test --- pom.xml | 6 ++++++ src/main/resources/application-test.properties | 9 ++++----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index 5a72606..b19bffd 100644 --- a/pom.xml +++ b/pom.xml @@ -49,6 +49,12 @@ org.postgresql postgresql + runtime + + + com.h2database + h2 + test org.projectlombok 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 From 95609be9313b7fd0eb0aa36ba4b943a1f3715097 Mon Sep 17 00:00:00 2001 From: AleksOBM Date: Fri, 27 Mar 2026 06:43:10 +0300 Subject: [PATCH 09/12] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B2=D0=BE=D0=B5=20?= =?UTF-8?q?=D1=80=D0=B5=D0=B2=D1=8C=D1=8E=2015-=D0=B3=D0=BE=20=D1=81=D0=BF?= =?UTF-8?q?=D1=80=D0=B8=D0=BD=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Попытка 6 пофиксить Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:3.2.5:test --- pom.xml | 1 - src/main/resources/application.properties | 10 +++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index b19bffd..c8c69e8 100644 --- a/pom.xml +++ b/pom.xml @@ -54,7 +54,6 @@ com.h2database h2 - test org.projectlombok diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index a97dc27..99debf5 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,10 +1,10 @@ spring.main.log-startup-info=false -spring.jpa.hibernate.ddl-auto=none +spring.jpa.hibernate.ddl-auto=validate -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.datasource.driver-class-name=org.h2.Driver +spring.datasource.url=jdbc:h2:file:./db/filmorate +spring.datasource.username=password +spring.datasource.password=sa hibernate.jdbc.time-zone=UTC hibernate.default-schema=public From 05c5a0614305dc9bce0078b5c705b2628dc46f40 Mon Sep 17 00:00:00 2001 From: AleksOBM Date: Fri, 27 Mar 2026 07:45:58 +0300 Subject: [PATCH 10/12] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B2=D0=BE=D0=B5=20?= =?UTF-8?q?=D1=80=D0=B5=D0=B2=D1=8C=D1=8E=2015-=D0=B3=D0=BE=20=D1=81=D0=BF?= =?UTF-8?q?=D1=80=D0=B8=D0=BD=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Попытка 7 пофиксить Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:3.2.5:test --- pom.xml | 1 + src/main/resources/application.properties | 13 ++++++------- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pom.xml b/pom.xml index c8c69e8..b19bffd 100644 --- a/pom.xml +++ b/pom.xml @@ -54,6 +54,7 @@ com.h2database h2 + test org.projectlombok diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 99debf5..4e8e69f 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,14 +1,13 @@ spring.main.log-startup-info=false +spring.sql.init.mode=always spring.jpa.hibernate.ddl-auto=validate -spring.datasource.driver-class-name=org.h2.Driver -spring.datasource.url=jdbc:h2:file:./db/filmorate -spring.datasource.username=password -spring.datasource.password=sa +spring.datasource.driver-class-name=org.postgresql.Driver +spring.datasource.url=jdbc:postgresql://localhost:5432/shareit +spring.datasource.username=dbuser +spring.datasource.password=12345 hibernate.jdbc.time-zone=UTC hibernate.default-schema=public -hibernate.show-sql=true -hibernate.format-sql=true -logging.level.root=error +logging.level.root=info From 6bdaab308d2610b817cf21034aad13ddbe884162 Mon Sep 17 00:00:00 2001 From: AleksOBM Date: Sat, 28 Mar 2026 12:31:32 +0300 Subject: [PATCH 11/12] =?UTF-8?q?=D0=92=D1=82=D0=BE=D1=80=D0=BE=D0=B5=20?= =?UTF-8?q?=D1=80=D0=B5=D0=B2=D1=8C=D1=8E=2015-=D0=B3=D0=BE=20=D1=81=D0=BF?= =?UTF-8?q?=D1=80=D0=B8=D0=BD=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 11 +++- .../shareit/booking/dto/BookingDto.java | 1 + .../shareit/booking/model/Booking.java | 13 ++-- .../booking/repository/BookingRepository.java | 17 ++++++ .../booking/service/BookingServiceImpl.java | 24 ++++++-- .../shareit/item/ItemController.java | 12 ++-- .../shareit/item/dto/CommentDto.java | 15 +---- .../shareit/item/dto/CommentMapper.java | 17 ++++++ ...WithDates.java => ItemDtoFullVersion.java} | 10 +++- .../shareit/item/dto/ItemDtoWithComments.java | 1 + .../shareit/item/dto/ItemMapper.java | 20 +------ .../practicum/shareit/item/model/Comment.java | 11 ++-- .../ru/practicum/shareit/item/model/Item.java | 13 ++-- .../shareit/item/service/ItemService.java | 7 +-- .../shareit/item/service/ItemServiceImpl.java | 59 +++++++++++-------- .../shareit/request/model/ItemRequest.java | 13 ++-- .../ru/practicum/shareit/user/model/User.java | 6 +- .../shareit/util/error/ErrorHandler.java | 6 ++ .../exception/BookingStatusException.java | 7 +++ src/main/resources/application.properties | 8 ++- src/main/resources/schema.sql | 15 +++-- 21 files changed, 174 insertions(+), 112 deletions(-) create mode 100644 src/main/java/ru/practicum/shareit/item/dto/CommentMapper.java rename src/main/java/ru/practicum/shareit/item/dto/{ItemDtoWithDates.java => ItemDtoFullVersion.java} (77%) create mode 100644 src/main/java/ru/practicum/shareit/util/exception/BookingStatusException.java diff --git a/pom.xml b/pom.xml index b19bffd..e05de59 100644 --- a/pom.xml +++ b/pom.xml @@ -20,6 +20,11 @@ + + org.projectlombok + lombok + true + org.springframework.boot spring-boot-starter-web @@ -57,9 +62,9 @@ test - org.projectlombok - lombok - true + org.zalando + logbook-spring-boot-starter + 3.9.0 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 454176e..1240807 100644 --- a/src/main/java/ru/practicum/shareit/booking/dto/BookingDto.java +++ b/src/main/java/ru/practicum/shareit/booking/dto/BookingDto.java @@ -29,6 +29,7 @@ public class BookingDto { rounding = TimeRoundingStrategy.TRUNCATE_TO_SECONDS, message = "Дата начала не может быть в прошлом" ) +// @FutureOrPresent(message = "Дата начала не может быть в прошлом") LocalDateTime start; @NotNull(groups = Marker.OnCreate.class, message = "Дата окончания бронирования - обязательна") diff --git a/src/main/java/ru/practicum/shareit/booking/model/Booking.java b/src/main/java/ru/practicum/shareit/booking/model/Booking.java index d807fea..164d6c0 100644 --- a/src/main/java/ru/practicum/shareit/booking/model/Booking.java +++ b/src/main/java/ru/practicum/shareit/booking/model/Booking.java @@ -1,10 +1,7 @@ package ru.practicum.shareit.booking.model; import jakarta.persistence.*; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; +import lombok.*; import lombok.experimental.FieldDefaults; import ru.practicum.shareit.item.model.Item; import ru.practicum.shareit.user.model.User; @@ -15,10 +12,12 @@ * TODO Sprint add-bookings. */ @Entity -@Table(name = "booking") -@Data -@AllArgsConstructor +@Getter +@Setter @NoArgsConstructor +@AllArgsConstructor +@Table(name = "bookings") +@EqualsAndHashCode(of = "id") @FieldDefaults(level = AccessLevel.PRIVATE) public class Booking { diff --git a/src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java b/src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java index 77a4374..a3895ba 100644 --- a/src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java +++ b/src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java @@ -5,6 +5,7 @@ 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; @@ -88,6 +89,22 @@ public interface BookingRepository extends JpaRepository { """) 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); diff --git a/src/main/java/ru/practicum/shareit/booking/service/BookingServiceImpl.java b/src/main/java/ru/practicum/shareit/booking/service/BookingServiceImpl.java index e0559c5..2017a10 100644 --- a/src/main/java/ru/practicum/shareit/booking/service/BookingServiceImpl.java +++ b/src/main/java/ru/practicum/shareit/booking/service/BookingServiceImpl.java @@ -16,6 +16,7 @@ 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; @@ -65,11 +66,26 @@ public BookingDto addNewBooking(Long userId, @NonNull BookingDto bookingDto) { public BookingDto approveBooking(long userId, long bookingId, boolean approved) { checkUser(userId); Booking booking = getBooking(bookingId); - if (approved) { - booking.setStatus(BookingStatus.APPROVED); - } else { - booking.setStatus(BookingStatus.REJECTED); + 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); } diff --git a/src/main/java/ru/practicum/shareit/item/ItemController.java b/src/main/java/ru/practicum/shareit/item/ItemController.java index 0ee4adf..c9f86d7 100644 --- a/src/main/java/ru/practicum/shareit/item/ItemController.java +++ b/src/main/java/ru/practicum/shareit/item/ItemController.java @@ -6,8 +6,7 @@ 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.ItemDtoWithComments; -import ru.practicum.shareit.item.dto.ItemDtoWithDates; +import ru.practicum.shareit.item.dto.ItemDtoFullVersion; import ru.practicum.shareit.item.service.ItemService; import ru.practicum.shareit.util.Marker; @@ -25,12 +24,15 @@ public class ItemController { private final ItemService itemService; @GetMapping("/{itemId}") - public ItemDtoWithComments 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); } diff --git a/src/main/java/ru/practicum/shareit/item/dto/CommentDto.java b/src/main/java/ru/practicum/shareit/item/dto/CommentDto.java index 958cc85..f52f008 100644 --- a/src/main/java/ru/practicum/shareit/item/dto/CommentDto.java +++ b/src/main/java/ru/practicum/shareit/item/dto/CommentDto.java @@ -2,16 +2,15 @@ import com.fasterxml.jackson.annotation.JsonProperty; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import lombok.Builder; import lombok.Data; -import org.springframework.lang.NonNull; -import ru.practicum.shareit.item.model.Comment; - import java.time.LocalDateTime; @Data @Builder +@NotNull public class CommentDto { Long id; @@ -25,14 +24,4 @@ public class CommentDto { @NotBlank String text; - - public static CommentDto from(@NonNull Comment comment) { - return CommentDto.builder() - .id(comment.getId()) - .itemId(comment.getItem().getId()) - .authorName(comment.getAuthor().getName()) - .createdDate(comment.getCreatedDate()) - .text(comment.getText()) - .build(); - } } 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..21019b5 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/dto/CommentMapper.java @@ -0,0 +1,17 @@ +package ru.practicum.shareit.item.dto; + +import org.springframework.lang.NonNull; +import ru.practicum.shareit.item.model.Comment; + +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(); + } +} diff --git a/src/main/java/ru/practicum/shareit/item/dto/ItemDtoWithDates.java b/src/main/java/ru/practicum/shareit/item/dto/ItemDtoFullVersion.java similarity index 77% rename from src/main/java/ru/practicum/shareit/item/dto/ItemDtoWithDates.java rename to src/main/java/ru/practicum/shareit/item/dto/ItemDtoFullVersion.java index 2d7f778..6cec023 100644 --- a/src/main/java/ru/practicum/shareit/item/dto/ItemDtoWithDates.java +++ b/src/main/java/ru/practicum/shareit/item/dto/ItemDtoFullVersion.java @@ -6,16 +6,17 @@ import lombok.experimental.FieldDefaults; import java.time.LocalDateTime; +import java.util.List; /** * TODO Sprint add-controllers. */ - @Data @NotNull @Builder @FieldDefaults(level = AccessLevel.PRIVATE) -public class ItemDtoWithDates { +public class ItemDtoFullVersion { + Long id; String name; String description; @@ -24,6 +25,11 @@ public class ItemDtoWithDates { @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/ItemDtoWithComments.java b/src/main/java/ru/practicum/shareit/item/dto/ItemDtoWithComments.java index 17359ee..e3bdc18 100644 --- a/src/main/java/ru/practicum/shareit/item/dto/ItemDtoWithComments.java +++ b/src/main/java/ru/practicum/shareit/item/dto/ItemDtoWithComments.java @@ -18,6 +18,7 @@ @Builder @FieldDefaults(level = AccessLevel.PRIVATE) public class ItemDtoWithComments { + Long id; String name; String description; 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 931126f..cc57dff 100644 --- a/src/main/java/ru/practicum/shareit/item/dto/ItemMapper.java +++ b/src/main/java/ru/practicum/shareit/item/dto/ItemMapper.java @@ -20,29 +20,13 @@ public static ItemDto toItemDto(@NonNull Item item) { ); } - public static ItemDtoWithDates toItemDtoWithDates( - @NonNull Item item, - LocalDateTime lastBookingDate, - LocalDateTime nextBookingDate - ) { - return ItemDtoWithDates.builder() - .id(item.getId()) - .name(item.getName()) - .description(item.getDescription()) - .available(item.isAvailable()) - .requestId(item.getRequest() != null ? item.getRequest().getId() : null) - .lastBookingDate(lastBookingDate) - .nextBookingDate(nextBookingDate) - .build(); - } - - public static ItemDtoWithComments toItemDtoWithComments( + public static ItemDtoFullVersion toItemDtoFull( @NonNull Item item, LocalDateTime lastBookingDate, LocalDateTime nextBookingDate, List comments ) { - return ItemDtoWithComments.builder() + return ItemDtoFullVersion.builder() .id(item.getId()) .name(item.getName()) .description(item.getDescription()) diff --git a/src/main/java/ru/practicum/shareit/item/model/Comment.java b/src/main/java/ru/practicum/shareit/item/model/Comment.java index 9bac980..c729767 100644 --- a/src/main/java/ru/practicum/shareit/item/model/Comment.java +++ b/src/main/java/ru/practicum/shareit/item/model/Comment.java @@ -1,20 +1,19 @@ package ru.practicum.shareit.item.model; import jakarta.persistence.*; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; +import lombok.*; import lombok.experimental.FieldDefaults; import ru.practicum.shareit.user.model.User; import java.time.LocalDateTime; @Entity -@Table(name = "comments") -@Data +@Getter +@Setter @NoArgsConstructor @AllArgsConstructor +@Table(name = "comments") +@EqualsAndHashCode(of = "id") @FieldDefaults(level = AccessLevel.PRIVATE) public class Comment { 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 be07991..8bf5762 100644 --- a/src/main/java/ru/practicum/shareit/item/model/Item.java +++ b/src/main/java/ru/practicum/shareit/item/model/Item.java @@ -1,11 +1,7 @@ package ru.practicum.shareit.item.model; import jakarta.persistence.*; -import jakarta.validation.constraints.NotNull; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; +import lombok.*; import lombok.experimental.FieldDefaults; import ru.practicum.shareit.request.model.ItemRequest; import ru.practicum.shareit.user.model.User; @@ -14,11 +10,12 @@ * TODO Sprint add-controllers. */ @Entity -@Table(name = "items") -@NotNull -@Data +@Getter +@Setter @NoArgsConstructor @AllArgsConstructor +@Table(name = "items") +@EqualsAndHashCode(of = "id") @FieldDefaults(level = AccessLevel.PRIVATE) public class Item { 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 068b346..81e1582 100644 --- a/src/main/java/ru/practicum/shareit/item/service/ItemService.java +++ b/src/main/java/ru/practicum/shareit/item/service/ItemService.java @@ -4,8 +4,7 @@ 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.ItemDtoWithComments; -import ru.practicum.shareit.item.dto.ItemDtoWithDates; +import ru.practicum.shareit.item.dto.ItemDtoFullVersion; import java.util.List; @@ -13,10 +12,10 @@ public interface ItemService { /// Получить вещь по id - ItemDtoWithComments getItem(long itemId); + ItemDtoFullVersion getItem(long userId, long itemId); /// Получить все вещи - List getAllItemsOfUser(long userId); + List getAllItemsOfUser(long userId); /// Поиск вещей по подстроке List search(long userId, String search); 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 3228f21..726ea18 100644 --- a/src/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java +++ b/src/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java @@ -18,7 +18,6 @@ import ru.practicum.shareit.util.exception.NotFoundException; import ru.practicum.shareit.util.exception.ParameterNotValidException; -import java.time.Duration; import java.time.LocalDateTime; import java.util.Collections; import java.util.List; @@ -37,22 +36,27 @@ public class ItemServiceImpl implements ItemService { CommentRepository commentRepository; @Override - public ItemDtoWithComments getItem(long itemId) { + public ItemDtoFullVersion getItem(long userId, long itemId) { Item item = getItemWithCheckPresent(itemId); - List bookings = bookingRepository.findAllBookingByItem_Id(item.getId()); - return ItemMapper.toItemDtoWithComments( + if (item.getOwner().getId() != userId) { + return ItemMapper.toItemDtoFull( + item, + null, + null, + getCommentDtos(itemId) + ); + } + return ItemMapper.toItemDtoFull( item, - getLastBookingDate(bookings), - getNextBookingDate(bookings), - commentRepository.findAllByItemId(itemId).stream() - .map(CommentDto::from) - .toList() + 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() @@ -60,10 +64,11 @@ public List getAllItemsOfUser(long userId) { List bookings = bookingRepository.findAllBookingByItemIdIn(items.keySet()); return items.values().stream() - .map(item -> ItemMapper.toItemDtoWithDates( + .map(item -> ItemMapper.toItemDtoFull( item, getLastBookingDate(bookings), - getNextBookingDate(bookings) + getNextBookingDate(bookings), + getCommentDtos(item.getId()) )).toList(); } @@ -108,9 +113,6 @@ public void deleteItem(long userId, long itemId) { @Override public CommentDto addComment(long userId, long itemId, @NonNull CommentDto commentDto) { String text = commentDto.getText(); - if (text == null || text.isBlank()) { - throw new ParameterNotValidException("text", "Текст комментария не может быть пустым"); - } User user = getUserWithCheckPresent(userId); Item item = getItemWithCheckPresent(itemId); if (item.getOwner().getId().equals(userId)) { @@ -121,10 +123,10 @@ public CommentDto addComment(long userId, long itemId, @NonNull CommentDto comme booking.getEnd().isBefore(LocalDateTime.now())) ) { throw new ParameterNotValidException( - "userId", "Комментарии доступны только тем, кто уже по пользовался вешью" + "userId", "Комментарии доступны только тем, кто уже по пользовался вещью" ); } - return CommentDto.from(commentRepository.save( + return CommentMapper.toDto(commentRepository.save( new Comment(null, item, user, LocalDateTime.now(), text.trim()) ) ); @@ -160,21 +162,26 @@ private Item getItemWithCheckPresentAndOwner(long itemId, long userId) { } private LocalDateTime getLastBookingDate(@NonNull List bookings) { + LocalDateTime now = LocalDateTime.now(); return bookings.stream() - .filter(booking -> Duration.between(booking.getStart(), booking.getEnd()).toMinutes() > 30) .map(Booking::getStart) - .filter(ldt -> ldt.isBefore(LocalDateTime.now()) || - ldt.isEqual(LocalDateTime.now())) - .sorted() - .findFirst().orElse(null); + .filter(start -> !start.isAfter(now)) + .max(LocalDateTime::compareTo) + .orElse(null); } private LocalDateTime getNextBookingDate(@NonNull List bookings) { + LocalDateTime now = LocalDateTime.now(); return bookings.stream() - .filter(booking -> Duration.between(booking.getStart(), booking.getEnd()).toMinutes() > 30) .map(Booking::getStart) - .filter(ldt -> ldt.isAfter(LocalDateTime.now())) - .sorted() - .findFirst().orElse(null); + .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/model/ItemRequest.java b/src/main/java/ru/practicum/shareit/request/model/ItemRequest.java index e1235f1..39f4bde 100644 --- a/src/main/java/ru/practicum/shareit/request/model/ItemRequest.java +++ b/src/main/java/ru/practicum/shareit/request/model/ItemRequest.java @@ -1,10 +1,7 @@ package ru.practicum.shareit.request.model; import jakarta.persistence.*; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; +import lombok.*; import lombok.experimental.FieldDefaults; import ru.practicum.shareit.user.model.User; @@ -14,10 +11,12 @@ * TODO Sprint add-item-requests. */ @Entity -@Table(name = "requests") -@Data -@AllArgsConstructor +@Getter +@Setter @NoArgsConstructor +@AllArgsConstructor +@Table(name = "requests") +@EqualsAndHashCode(of = "id") @FieldDefaults(level = AccessLevel.PRIVATE) public class ItemRequest { 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 db579bc..8b9da80 100644 --- a/src/main/java/ru/practicum/shareit/user/model/User.java +++ b/src/main/java/ru/practicum/shareit/user/model/User.java @@ -5,10 +5,12 @@ import lombok.experimental.FieldDefaults; @Entity -@Table(name = "users") -@Data +@Getter +@Setter @NoArgsConstructor @AllArgsConstructor +@Table(name = "users") +@EqualsAndHashCode(of = "id") @FieldDefaults(level = AccessLevel.PRIVATE) public class User { 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 7852307..6b9f82b 100644 --- a/src/main/java/ru/practicum/shareit/util/error/ErrorHandler.java +++ b/src/main/java/ru/practicum/shareit/util/error/ErrorHandler.java @@ -50,6 +50,12 @@ public ErrorResponse handleParameterNotValid(final ParameterNotValidException e) ); } + @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/resources/application.properties b/src/main/resources/application.properties index 4e8e69f..46af283 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,6 +1,8 @@ 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 spring.datasource.driver-class-name=org.postgresql.Driver spring.datasource.url=jdbc:postgresql://localhost:5432/shareit @@ -9,5 +11,9 @@ spring.datasource.password=12345 hibernate.jdbc.time-zone=UTC hibernate.default-schema=public +hibernate.show-sql=true +hibernate.format-sql=true -logging.level.root=info +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 index 3fca937..2763290 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -2,9 +2,9 @@ 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), - CONSTRAINT uq_user_email UNIQUE (email) + 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, @@ -26,18 +26,21 @@ CREATE TABLE IF NOT EXISTS public.items ( 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.booking ( +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_booking PRIMARY KEY (id), - CONSTRAINT fk_booking_items_id FOREIGN KEY (item_id) REFERENCES public.items(id) ON DELETE CASCADE, - CONSTRAINT fk_booking_users_id FOREIGN KEY (booker_id) REFERENCES public.users(id) ON DELETE CASCADE + 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, From ce4fb114b8a9a5ba4bb1821ad8cd1bbbb3b9f788 Mon Sep 17 00:00:00 2001 From: AleksOBM Date: Sun, 29 Mar 2026 13:04:29 +0300 Subject: [PATCH 12/12] =?UTF-8?q?=D0=A2=D1=80=D0=B5=D1=82=D1=8C=D0=B5=20?= =?UTF-8?q?=D1=80=D0=B5=D0=B2=D1=8C=D1=8E=2015-=D0=B3=D0=BE=20=D1=81=D0=BF?= =?UTF-8?q?=D1=80=D0=B8=D0=BD=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +- SchemaDatabase.png | Bin 0 -> 74333 bytes lombok.config | 8 ++- .../shareit/booking/BookingController.java | 5 +- .../shareit/booking/dto/BookingDto.java | 57 +++--------------- .../shareit/booking/dto/BookingMapper.java | 28 ++++----- .../booking/dto/IncomingBookingDto.java | 23 +++++++ .../shareit/booking/model/Booking.java | 32 ++++++++-- .../booking/repository/BookingRepository.java | 2 +- .../booking/service/BookingService.java | 3 +- .../booking/service/BookingServiceImpl.java | 17 +++--- .../shareit/item/ItemController.java | 2 +- .../shareit/item/dto/CommentMapper.java | 13 ++++ .../practicum/shareit/item/dto/ItemDto.java | 13 +--- .../shareit/item/dto/ItemDtoFullVersion.java | 2 +- .../shareit/item/dto/ItemDtoWithComments.java | 37 ------------ .../shareit/item/dto/ItemMapper.java | 33 +++++----- .../practicum/shareit/item/model/Comment.java | 31 ++++++++-- .../ru/practicum/shareit/item/model/Item.java | 32 ++++++++-- .../item/repository/CommentRepository.java | 4 ++ .../shareit/item/service/ItemServiceImpl.java | 52 +++++++++------- .../shareit/request/model/ItemRequest.java | 14 ++--- .../shareit/user/UserController.java | 3 +- .../practicum/shareit/user/dto/UserDto.java | 11 +--- .../shareit/user/dto/UserMapper.java | 19 +++--- .../ru/practicum/shareit/user/model/User.java | 34 ++++++++--- .../shareit/user/service/UserServiceImpl.java | 2 +- ...FutureOrPresentWithToleranceValidator.java | 6 +- .../hibernate/HibernateEqualsAndHashCode.java | 13 ++++ 29 files changed, 277 insertions(+), 223 deletions(-) create mode 100644 SchemaDatabase.png create mode 100644 src/main/java/ru/practicum/shareit/booking/dto/IncomingBookingDto.java delete mode 100644 src/main/java/ru/practicum/shareit/item/dto/ItemDtoWithComments.java create mode 100644 src/main/java/ru/practicum/shareit/util/hibernate/HibernateEqualsAndHashCode.java 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 0000000000000000000000000000000000000000..1f547e72495f6810f2554d0c5afaf59d604b3754 GIT binary patch literal 74333 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 literal 0 HcmV?d00001 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/src/main/java/ru/practicum/shareit/booking/BookingController.java b/src/main/java/ru/practicum/shareit/booking/BookingController.java index 629a782..84bbaa7 100644 --- a/src/main/java/ru/practicum/shareit/booking/BookingController.java +++ b/src/main/java/ru/practicum/shareit/booking/BookingController.java @@ -5,6 +5,7 @@ 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; @@ -18,7 +19,7 @@ @RequestMapping(path = "/bookings") @RequiredArgsConstructor public class BookingController { - private final BookingService bookingService; + final BookingService bookingService; @GetMapping("/{bookingId}") public BookingDto getBooking( @@ -48,7 +49,7 @@ public List getAllBookingsByOwner( @Validated(Marker.OnCreate.class) public BookingDto addNewBooking( @RequestHeader("X-Sharer-User-Id") Long userId, - @Valid @RequestBody BookingDto bookingDto + @Valid @RequestBody IncomingBookingDto bookingDto ) { return bookingService.addNewBooking(userId, bookingDto); } 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 1240807..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,62 +1,21 @@ package ru.practicum.shareit.booking.dto; -import jakarta.validation.constraints.Future; import jakarta.validation.constraints.NotNull; -import lombok.AccessLevel; -import lombok.Data; -import lombok.experimental.FieldDefaults; import ru.practicum.shareit.booking.model.BookingStatus; import ru.practicum.shareit.item.dto.ItemDto; import ru.practicum.shareit.user.dto.UserDto; -import ru.practicum.shareit.util.Marker; -import ru.practicum.shareit.util.datetime.FutureOrPresentWithTolerance; -import ru.practicum.shareit.util.datetime.TimeRoundingStrategy; import java.time.LocalDateTime; /** * TODO Sprint add-bookings. */ -@Data @NotNull -@FieldDefaults(level = AccessLevel.PRIVATE) -public class BookingDto { - - Long id; - - @NotNull(groups = Marker.OnCreate.class, message = "Дата начала бронирования - обязательна") - @FutureOrPresentWithTolerance( - rounding = TimeRoundingStrategy.TRUNCATE_TO_SECONDS, - message = "Дата начала не может быть в прошлом" - ) -// @FutureOrPresent(message = "Дата начала не может быть в прошлом") - LocalDateTime start; - - @NotNull(groups = Marker.OnCreate.class, message = "Дата окончания бронирования - обязательна") - @Future(message = "Дата окончания должна быть в будущем") - LocalDateTime end; - - ItemDto item; - - UserDto booker; - - BookingStatus status; - - public BookingDto( - Long id, - LocalDateTime start, - LocalDateTime end, - Long itemId, - Long bookerId, - BookingStatus status - ) { - this.id = id != null && id <= 0 ? null : id; - this.start = start; - this.end = end; - this.item = itemId != null && itemId <= 0 ? null : - new ItemDto(itemId, null, null, null, null); - this.booker = bookerId != null && bookerId <= 0 ? null : - new UserDto(bookerId, null, null); - this.status = status; - } -} +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 index e707763..9520b4a 100644 --- a/src/main/java/ru/practicum/shareit/booking/dto/BookingMapper.java +++ b/src/main/java/ru/practicum/shareit/booking/dto/BookingMapper.java @@ -2,6 +2,7 @@ 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; @@ -13,28 +14,23 @@ public class BookingMapper { @NonNull public static BookingDto toBookingDto(@NonNull Booking booking) { - BookingDto bookingDto = new BookingDto( + return new BookingDto( booking.getId(), booking.getStart().atZone(ZoneId.systemDefault()).toLocalDateTime(), booking.getEnd().atZone(ZoneId.systemDefault()).toLocalDateTime(), - null, - null, + ItemMapper.toItemDto(booking.getItem()), + UserMapper.toUserDto(booking.getBooker()), booking.getStatus() ); - bookingDto.setItem(ItemMapper.toItemDto(booking.getItem())); - bookingDto.setBooker(UserMapper.toUserDto(booking.getBooker())); - return bookingDto; } - @NonNull - public static Booking toBooking(@NonNull BookingDto bookingDto, Item item, User booker) { - return new Booking( - bookingDto.getId(), - bookingDto.getStart().atZone(ZoneId.systemDefault()).toLocalDateTime(), - bookingDto.getEnd().atZone(ZoneId.systemDefault()).toLocalDateTime(), - item, - booker, - bookingDto.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 index 164d6c0..b0d322f 100644 --- a/src/main/java/ru/practicum/shareit/booking/model/Booking.java +++ b/src/main/java/ru/practicum/shareit/booking/model/Booking.java @@ -2,23 +2,27 @@ import jakarta.persistence.*; import lombok.*; -import lombok.experimental.FieldDefaults; +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 -@Getter -@Setter @NoArgsConstructor -@AllArgsConstructor +@Accessors(chain = true) @Table(name = "bookings") -@EqualsAndHashCode(of = "id") -@FieldDefaults(level = AccessLevel.PRIVATE) +@ExtensionMethod({HibernateEqualsAndHashCode.class}) public class Booking { @Id @@ -41,4 +45,20 @@ public class Booking { @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/repository/BookingRepository.java b/src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java index a3895ba..08fb87b 100644 --- a/src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java +++ b/src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java @@ -14,7 +14,7 @@ public interface BookingRepository extends JpaRepository { @Query(""" select b from Booking b - left join fetch Item i on i.id = b.item.id + left join fetch Item i on b.item.id = i.id where i.owner.id = :ownerId order by b.start desc """) diff --git a/src/main/java/ru/practicum/shareit/booking/service/BookingService.java b/src/main/java/ru/practicum/shareit/booking/service/BookingService.java index f97d11c..3abc913 100644 --- a/src/main/java/ru/practicum/shareit/booking/service/BookingService.java +++ b/src/main/java/ru/practicum/shareit/booking/service/BookingService.java @@ -3,6 +3,7 @@ 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; @@ -20,7 +21,7 @@ public interface BookingService { /// Добавление нового бронирования @Transactional - BookingDto addNewBooking(Long userId, BookingDto bookingDto); + BookingDto addNewBooking(Long userId, IncomingBookingDto bookingDto); /// Согласование бронирования @Transactional(propagation = Propagation.REQUIRED) diff --git a/src/main/java/ru/practicum/shareit/booking/service/BookingServiceImpl.java b/src/main/java/ru/practicum/shareit/booking/service/BookingServiceImpl.java index 2017a10..2cbfcbf 100644 --- a/src/main/java/ru/practicum/shareit/booking/service/BookingServiceImpl.java +++ b/src/main/java/ru/practicum/shareit/booking/service/BookingServiceImpl.java @@ -1,13 +1,13 @@ package ru.practicum.shareit.booking.service; import jakarta.validation.ValidationException; -import lombok.AccessLevel; 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; @@ -26,7 +26,7 @@ @Service @RequiredArgsConstructor -@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) +@FieldDefaults(makeFinal = true) public class BookingServiceImpl implements BookingService { BookingRepository bookingRepository; @@ -34,19 +34,18 @@ public class BookingServiceImpl implements BookingService { UserRepository userRepository; @Override - public BookingDto addNewBooking(Long userId, @NonNull BookingDto bookingDto) { + 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("Дата начала бронирования должна быть раньше даты окончания"); } - if (bookingDto.getItem() == null) { - throw new ValidationException("item.id не может быть null"); - } + User booker = userRepository.findById(userId).orElseThrow( () -> new ForbiddenException("Пользователь с id=" + userId + " не найден") ); - Long itemId = bookingDto.getItem().getId(); + + long itemId = bookingDto.getItemId(); Item item = itemRepository.findById(itemId).orElseThrow( () -> new NotFoundException("Вещь с id=" + itemId + " не найдена") ); @@ -56,9 +55,9 @@ public BookingDto addNewBooking(Long userId, @NonNull BookingDto bookingDto) { if (!item.isAvailable()) { throw new ValidationException("Вещь с id=" + item.getId() + " не доступна для аренды"); } - bookingDto.setStatus(BookingStatus.WAITING); + return BookingMapper.toBookingDto( - bookingRepository.save(BookingMapper.toBooking(bookingDto, item, booker)) + bookingRepository.save(BookingMapper.fromIncomingDto(bookingDto, item, booker)) ); } diff --git a/src/main/java/ru/practicum/shareit/item/ItemController.java b/src/main/java/ru/practicum/shareit/item/ItemController.java index c9f86d7..be70df4 100644 --- a/src/main/java/ru/practicum/shareit/item/ItemController.java +++ b/src/main/java/ru/practicum/shareit/item/ItemController.java @@ -21,7 +21,7 @@ @RequiredArgsConstructor public class ItemController { - private final ItemService itemService; + final ItemService itemService; @GetMapping("/{itemId}") public ItemDtoFullVersion getItem( diff --git a/src/main/java/ru/practicum/shareit/item/dto/CommentMapper.java b/src/main/java/ru/practicum/shareit/item/dto/CommentMapper.java index 21019b5..a6650e0 100644 --- a/src/main/java/ru/practicum/shareit/item/dto/CommentMapper.java +++ b/src/main/java/ru/practicum/shareit/item/dto/CommentMapper.java @@ -2,6 +2,10 @@ 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 { @@ -14,4 +18,13 @@ public static CommentDto toDto(@NonNull Comment comment) { .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 6236682..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,7 +12,8 @@ */ @Data @NotNull -@FieldDefaults(level = AccessLevel.PRIVATE) +@Builder +@FieldDefaults(makeFinal = true) public class ItemDto { Long id; @@ -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 index 6cec023..caba5a3 100644 --- a/src/main/java/ru/practicum/shareit/item/dto/ItemDtoFullVersion.java +++ b/src/main/java/ru/practicum/shareit/item/dto/ItemDtoFullVersion.java @@ -14,7 +14,7 @@ @Data @NotNull @Builder -@FieldDefaults(level = AccessLevel.PRIVATE) +@FieldDefaults(makeFinal = true) public class ItemDtoFullVersion { Long id; diff --git a/src/main/java/ru/practicum/shareit/item/dto/ItemDtoWithComments.java b/src/main/java/ru/practicum/shareit/item/dto/ItemDtoWithComments.java deleted file mode 100644 index e3bdc18..0000000 --- a/src/main/java/ru/practicum/shareit/item/dto/ItemDtoWithComments.java +++ /dev/null @@ -1,37 +0,0 @@ -package ru.practicum.shareit.item.dto; - -import com.fasterxml.jackson.annotation.JsonProperty; -import jakarta.validation.constraints.NotNull; -import lombok.AccessLevel; -import lombok.Builder; -import lombok.Data; -import lombok.experimental.FieldDefaults; - -import java.time.LocalDateTime; -import java.util.List; - -/** - * TODO Sprint add-controllers. - */ -@Data -@NotNull -@Builder -@FieldDefaults(level = AccessLevel.PRIVATE) -public class ItemDtoWithComments { - - 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 cc57dff..cfb2ee2 100644 --- a/src/main/java/ru/practicum/shareit/item/dto/ItemMapper.java +++ b/src/main/java/ru/practicum/shareit/item/dto/ItemMapper.java @@ -11,16 +11,16 @@ public class ItemMapper { @NonNull public static ItemDto toItemDto(@NonNull Item item) { - return new ItemDto( - item.getId(), - item.getName(), - item.getDescription(), - item.isAvailable(), - item.getRequest() != null ? item.getRequest().getId() : null - ); + 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 toItemDtoFull( + public static ItemDtoFullVersion toFullItemDto( @NonNull Item item, LocalDateTime lastBookingDate, LocalDateTime nextBookingDate, @@ -39,14 +39,13 @@ public static ItemDtoFullVersion toItemDtoFull( } @NonNull - public static Item toItem(@NonNull ItemDto itemDto, User user) { - return new Item( - itemDto.getId(), - itemDto.getName(), - itemDto.getDescription(), - itemDto.getAvailable(), - user, - null - ); + 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 index c729767..6c965e9 100644 --- a/src/main/java/ru/practicum/shareit/item/model/Comment.java +++ b/src/main/java/ru/practicum/shareit/item/model/Comment.java @@ -2,19 +2,22 @@ import jakarta.persistence.*; import lombok.*; -import lombok.experimental.FieldDefaults; +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 -@Getter -@Setter @NoArgsConstructor -@AllArgsConstructor +@Accessors(chain = true) @Table(name = "comments") -@EqualsAndHashCode(of = "id") -@FieldDefaults(level = AccessLevel.PRIVATE) +@ExtensionMethod({HibernateEqualsAndHashCode.class}) public class Comment { @Id @@ -33,4 +36,20 @@ public class Comment { @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 8bf5762..f17ad6b 100644 --- a/src/main/java/ru/practicum/shareit/item/model/Item.java +++ b/src/main/java/ru/practicum/shareit/item/model/Item.java @@ -2,21 +2,25 @@ import jakarta.persistence.*; import lombok.*; -import lombok.experimental.FieldDefaults; +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 @Entity -@Getter -@Setter @NoArgsConstructor -@AllArgsConstructor +@Accessors(chain = true) @Table(name = "items") -@EqualsAndHashCode(of = "id") -@FieldDefaults(level = AccessLevel.PRIVATE) +@ExtensionMethod({HibernateEqualsAndHashCode.class}) public class Item { @Id @@ -40,4 +44,20 @@ public class Item { @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 index fcb4f88..2dc4678 100644 --- a/src/main/java/ru/practicum/shareit/item/repository/CommentRepository.java +++ b/src/main/java/ru/practicum/shareit/item/repository/CommentRepository.java @@ -4,7 +4,11 @@ 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/service/ItemServiceImpl.java b/src/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java index 726ea18..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,6 +1,5 @@ package ru.practicum.shareit.item.service; -import lombok.AccessLevel; import lombok.RequiredArgsConstructor; import lombok.experimental.FieldDefaults; import org.springframework.lang.NonNull; @@ -19,6 +18,7 @@ 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; @@ -26,8 +26,8 @@ import java.util.stream.Collectors; @Service -@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) @RequiredArgsConstructor +@FieldDefaults(makeFinal = true) public class ItemServiceImpl implements ItemService { ItemRepository itemRepository; @@ -40,19 +40,19 @@ public ItemDtoFullVersion getItem(long userId, long itemId) { Item item = getItemWithCheckPresent(itemId); if (item.getOwner().getId() != userId) { - return ItemMapper.toItemDtoFull( + return ItemMapper.toFullItemDto( item, null, null, getCommentDtos(itemId) ); } - return ItemMapper.toItemDtoFull( + return ItemMapper.toFullItemDto( item, bookingRepository.findLastBookingDate(itemId), bookingRepository.findNextBookingDate(itemId), getCommentDtos(itemId) - ); + ); } @Override @@ -62,14 +62,24 @@ public List getAllItemsOfUser(long 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())); - return items.values().stream() - .map(item -> ItemMapper.toItemDtoFull( - item, - getLastBookingDate(bookings), - getNextBookingDate(bookings), - getCommentDtos(item.getId()) - )).toList(); + 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 result; } @Override @@ -92,14 +102,13 @@ public ItemDto addNewItem(Long userId, 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 - ); + 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)); } @@ -112,7 +121,6 @@ public void deleteItem(long userId, long itemId) { @Override public CommentDto addComment(long userId, long itemId, @NonNull CommentDto commentDto) { - String text = commentDto.getText(); User user = getUserWithCheckPresent(userId); Item item = getItemWithCheckPresent(itemId); if (item.getOwner().getId().equals(userId)) { @@ -127,7 +135,7 @@ public CommentDto addComment(long userId, long itemId, @NonNull CommentDto comme ); } return CommentMapper.toDto(commentRepository.save( - new Comment(null, item, user, LocalDateTime.now(), text.trim()) + CommentMapper.toComment(commentDto, item, user) ) ); } diff --git a/src/main/java/ru/practicum/shareit/request/model/ItemRequest.java b/src/main/java/ru/practicum/shareit/request/model/ItemRequest.java index 39f4bde..2bb0df7 100644 --- a/src/main/java/ru/practicum/shareit/request/model/ItemRequest.java +++ b/src/main/java/ru/practicum/shareit/request/model/ItemRequest.java @@ -2,22 +2,22 @@ import jakarta.persistence.*; import lombok.*; -import lombok.experimental.FieldDefaults; +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 -@Getter -@Setter -@NoArgsConstructor -@AllArgsConstructor @Table(name = "requests") -@EqualsAndHashCode(of = "id") -@FieldDefaults(level = AccessLevel.PRIVATE) +@NoArgsConstructor +@Accessors(chain = true) +@ExtensionMethod({HibernateEqualsAndHashCode.class}) public class ItemRequest { @Id 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 559a3df..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,14 +1,15 @@ 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; @@ -21,10 +22,4 @@ public class UserDto { @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 79b7cd1..3a47070 100644 --- a/src/main/java/ru/practicum/shareit/user/dto/UserMapper.java +++ b/src/main/java/ru/practicum/shareit/user/dto/UserMapper.java @@ -5,18 +5,17 @@ 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 index 8b9da80..c719c35 100644 --- a/src/main/java/ru/practicum/shareit/user/model/User.java +++ b/src/main/java/ru/practicum/shareit/user/model/User.java @@ -2,16 +2,20 @@ import jakarta.persistence.*; import lombok.*; -import lombok.experimental.FieldDefaults; +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 -@Getter -@Setter -@NoArgsConstructor -@AllArgsConstructor @Table(name = "users") -@EqualsAndHashCode(of = "id") -@FieldDefaults(level = AccessLevel.PRIVATE) +@NoArgsConstructor +@Accessors(chain = true) +@ExtensionMethod({HibernateEqualsAndHashCode.class}) public class User { @Id @@ -23,4 +27,20 @@ public class User { @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/service/UserServiceImpl.java b/src/main/java/ru/practicum/shareit/user/service/UserServiceImpl.java index b0f82aa..6bfc640 100644 --- a/src/main/java/ru/practicum/shareit/user/service/UserServiceImpl.java +++ b/src/main/java/ru/practicum/shareit/user/service/UserServiceImpl.java @@ -15,7 +15,7 @@ @RequiredArgsConstructor class UserServiceImpl implements UserService { - private final UserRepository userRepository; + final UserRepository userRepository; @Override public UserDto getUser(long userId) { diff --git a/src/main/java/ru/practicum/shareit/util/datetime/FutureOrPresentWithToleranceValidator.java b/src/main/java/ru/practicum/shareit/util/datetime/FutureOrPresentWithToleranceValidator.java index 0cd746b..e4147a6 100644 --- a/src/main/java/ru/practicum/shareit/util/datetime/FutureOrPresentWithToleranceValidator.java +++ b/src/main/java/ru/practicum/shareit/util/datetime/FutureOrPresentWithToleranceValidator.java @@ -9,9 +9,9 @@ public class FutureOrPresentWithToleranceValidator implements ConstraintValidator { - private long toleranceMillis; - private String zoneIdStr; - private TimeRoundingStrategy roundingStrategy; + long toleranceMillis; + String zoneIdStr; + TimeRoundingStrategy roundingStrategy; @Override public void initialize(FutureOrPresentWithTolerance constraintAnnotation) { 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(); + } +}