From 518cce0365578bf7e13a002becd532ab78e215d4 Mon Sep 17 00:00:00 2001 From: Veselov Nikolay Date: Tue, 30 Jan 2024 17:10:00 +0300 Subject: [PATCH] feature/45: paging --- .../companybot/annotation/PagingParam.java | 15 ++++++ .../config/PagingRequestParamsResolver.java | 52 +++++++++++++++++++ .../config/WebMvcConfiguration.java | 36 +++++++++++++ .../controller/InquiryController.java | 26 ++++++---- .../companybot/dto/ContactResponseDto.java | 4 ++ .../companybot/dto/CustomerResponseDTO.java | 8 ++- .../companybot/dto/InquiryResponseDTO.java | 7 +++ .../companybot/dto/MessageResponseDTO.java | 3 ++ .../veselov/companybot/dto/PagingParams.java | 17 ++++++ .../companybot/mapper/InquiryMapper.java | 7 +-- .../companybot/model/DivisionModel.java | 6 +-- .../companybot/service/InquiryService.java | 4 +- .../service/impl/InquiryServiceImpl.java | 10 ++-- .../InquiryControllerIntegrationTest.java | 25 +++++++++ .../service/impl/InquiryServiceImplTest.java | 19 ++++--- 15 files changed, 212 insertions(+), 27 deletions(-) create mode 100644 src/main/java/ru/veselov/companybot/annotation/PagingParam.java create mode 100644 src/main/java/ru/veselov/companybot/config/PagingRequestParamsResolver.java create mode 100644 src/main/java/ru/veselov/companybot/config/WebMvcConfiguration.java create mode 100644 src/main/java/ru/veselov/companybot/dto/PagingParams.java create mode 100644 src/test/java/ru/veselov/companybot/it/controller/InquiryControllerIntegrationTest.java diff --git a/src/main/java/ru/veselov/companybot/annotation/PagingParam.java b/src/main/java/ru/veselov/companybot/annotation/PagingParam.java new file mode 100644 index 0000000..2543a2b --- /dev/null +++ b/src/main/java/ru/veselov/companybot/annotation/PagingParam.java @@ -0,0 +1,15 @@ +package ru.veselov.companybot.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Mark object that receives Request Params: page и size + * and send it to PagingRequestParamResolver + */ +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface PagingParam { +} \ No newline at end of file diff --git a/src/main/java/ru/veselov/companybot/config/PagingRequestParamsResolver.java b/src/main/java/ru/veselov/companybot/config/PagingRequestParamsResolver.java new file mode 100644 index 0000000..a089c20 --- /dev/null +++ b/src/main/java/ru/veselov/companybot/config/PagingRequestParamsResolver.java @@ -0,0 +1,52 @@ +package ru.veselov.companybot.config; + +import org.springframework.core.MethodParameter; +import org.springframework.lang.NonNull; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.annotation.RequestParamMethodArgumentResolver; +import ru.veselov.companybot.annotation.PagingParam; +import ru.veselov.companybot.dto.PagingParams; + +/** + * Resolve request params to object marked with {@link PagingParam} annotation + * + * @see PagingParam + * @see PagingParams + */ +public class PagingRequestParamsResolver extends RequestParamMethodArgumentResolver { + + private static final String PAGE = "page"; + + private static final String SIZE = "size"; + + + public PagingRequestParamsResolver(boolean useDefaultResolution) { + super(useDefaultResolution); + } + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(PagingParam.class); + } + + @Override + protected Object resolveName(@NonNull String name, + @NonNull MethodParameter parameter, + @NonNull NativeWebRequest request) { + return new PagingParams( + resolvePage(request), + resolveSize(request) + ); + } + + private Integer resolvePage(NativeWebRequest request) { + String page = request.getParameter(PAGE); + return page == null ? 0 : Integer.parseInt(page); + } + + private Integer resolveSize(NativeWebRequest request) { + String size = request.getParameter(SIZE); + return size == null ? 20 : Integer.parseInt(size); + } + +} diff --git a/src/main/java/ru/veselov/companybot/config/WebMvcConfiguration.java b/src/main/java/ru/veselov/companybot/config/WebMvcConfiguration.java new file mode 100644 index 0000000..3520bee --- /dev/null +++ b/src/main/java/ru/veselov/companybot/config/WebMvcConfiguration.java @@ -0,0 +1,36 @@ +package ru.veselov.companybot.config; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.lang.NonNull; +import org.springframework.web.filter.CommonsRequestLoggingFilter; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.List; + +@Configuration +@Slf4j +public class WebMvcConfiguration implements WebMvcConfigurer { + + @Override + public void addArgumentResolvers(@NonNull List resolvers) { + WebMvcConfigurer.super.addArgumentResolvers(resolvers); + resolvers.add(new PagingRequestParamsResolver(true)); + log.debug("Setting up Request Param resolvers"); + } + + @Bean + public CommonsRequestLoggingFilter logFilter() { + CommonsRequestLoggingFilter filter = new CommonsRequestLoggingFilter(); + filter.setIncludeQueryString(true); + filter.setIncludePayload(false); + filter.setMaxPayloadLength(10000); + filter.setIncludeHeaders(false); + filter.setIncludeClientInfo(true); + log.debug("Setting up Request filter for logging http request/response"); + return filter; + } + +} diff --git a/src/main/java/ru/veselov/companybot/controller/InquiryController.java b/src/main/java/ru/veselov/companybot/controller/InquiryController.java index 573e4ce..8211bcf 100644 --- a/src/main/java/ru/veselov/companybot/controller/InquiryController.java +++ b/src/main/java/ru/veselov/companybot/controller/InquiryController.java @@ -1,19 +1,24 @@ package ru.veselov.companybot.controller; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; import org.springframework.http.MediaType; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import ru.veselov.companybot.annotation.PagingParam; import ru.veselov.companybot.dto.InquiryResponseDTO; +import ru.veselov.companybot.dto.PagingParams; import ru.veselov.companybot.service.InquiryService; import java.util.List; @@ -27,16 +32,17 @@ public class InquiryController { private final InquiryService inquiryService; - @Operation(summary = "Добавить новый отдел", - description = "Принимает название и описание отдела") - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "Отдел создан", - content = {@Content(array = @ArraySchema(schema = @Schema(implementation = InquiryResponseDTO.class)), - mediaType = MediaType.APPLICATION_JSON_VALUE)}) - }) + @Operation(summary = "Получить все запросы", + description = "Выгружает все запросы", + parameters = { + @Parameter(in = ParameterIn.QUERY, name = "page"), + @Parameter(in = ParameterIn.QUERY, name = "size")}) + @ApiResponse(responseCode = "200", description = "Запросы успешно получены", + content = {@Content(array = @ArraySchema(schema = @Schema(implementation = InquiryResponseDTO.class)), + mediaType = MediaType.APPLICATION_JSON_VALUE)}) @GetMapping - public List getAll() { - return inquiryService.findAll(); + public Page getAll(@Schema(hidden = true) @Valid @PagingParam PagingParams pagingParams) { + return inquiryService.findAll(pagingParams); } } diff --git a/src/main/java/ru/veselov/companybot/dto/ContactResponseDto.java b/src/main/java/ru/veselov/companybot/dto/ContactResponseDto.java index 96116fa..a3ccde7 100644 --- a/src/main/java/ru/veselov/companybot/dto/ContactResponseDto.java +++ b/src/main/java/ru/veselov/companybot/dto/ContactResponseDto.java @@ -1,5 +1,6 @@ package ru.veselov.companybot.dto; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -15,10 +16,13 @@ @AllArgsConstructor public class ContactResponseDto implements Serializable { + @Schema(description = "Id контакта", example = "8b19a7cf-67f2-47b3-9ddb-9e3d9514d375") private UUID contactId; + @Schema(description = "Телефонный номер", example = "+7 916 555 55 55") private String phone; + @Schema(description = "E-mail", example = "email@email.com") private String email; } diff --git a/src/main/java/ru/veselov/companybot/dto/CustomerResponseDTO.java b/src/main/java/ru/veselov/companybot/dto/CustomerResponseDTO.java index 9ef3355..9254dec 100644 --- a/src/main/java/ru/veselov/companybot/dto/CustomerResponseDTO.java +++ b/src/main/java/ru/veselov/companybot/dto/CustomerResponseDTO.java @@ -1,5 +1,7 @@ package ru.veselov.companybot.dto; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -14,15 +16,19 @@ @NoArgsConstructor @AllArgsConstructor public class CustomerResponseDTO implements Serializable { - + @Schema(description = "Id клиента", example = "1000") private Long id; + @Schema(description = "Имя клиента", example = "Иван") private String firstName; + @Schema(description = "Фамилия клиента", example = "Петров") private String lastName; + @Schema(description = "Юзернейм клиента", example = "Ivan") private String userName; + @ArraySchema(arraySchema = @Schema(implementation = ContactResponseDto.class, description = "Контакты")) private Set contacts; } diff --git a/src/main/java/ru/veselov/companybot/dto/InquiryResponseDTO.java b/src/main/java/ru/veselov/companybot/dto/InquiryResponseDTO.java index f7106d2..fc017c2 100644 --- a/src/main/java/ru/veselov/companybot/dto/InquiryResponseDTO.java +++ b/src/main/java/ru/veselov/companybot/dto/InquiryResponseDTO.java @@ -1,5 +1,7 @@ package ru.veselov.companybot.dto; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -17,14 +19,19 @@ @ToString public class InquiryResponseDTO implements Serializable { + @Schema(description = "Id запроса", example = "d15a43eb-0bb9-4b15-ac3a-abdf3b77137f") private UUID inquiryId; + @Schema(description = "Дата запроса", example = "130f4ee4-037f-47b3-9b63-8c3b0ac02574") private LocalDateTime date; + @Schema(implementation = DivisionModel.class, description = "Отдел") private DivisionModel division; + @ArraySchema(arraySchema = @Schema(implementation = MessageResponseDTO.class, description = "Сообщения")) private Set messages; + @Schema(implementation = CustomerResponseDTO.class, description = "Клиент") private CustomerResponseDTO customer; } diff --git a/src/main/java/ru/veselov/companybot/dto/MessageResponseDTO.java b/src/main/java/ru/veselov/companybot/dto/MessageResponseDTO.java index a7c5437..8ec2db8 100644 --- a/src/main/java/ru/veselov/companybot/dto/MessageResponseDTO.java +++ b/src/main/java/ru/veselov/companybot/dto/MessageResponseDTO.java @@ -1,5 +1,6 @@ package ru.veselov.companybot.dto; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -15,8 +16,10 @@ @AllArgsConstructor public class MessageResponseDTO implements Serializable { + @Schema(description = "Id сообщения", example = "3b4cb719-3489-445d-bb01-ef7958aca896") private Integer messageId; + @Schema(description = "Текст сообщения", example = "Перезвоните мне") private String text; } diff --git a/src/main/java/ru/veselov/companybot/dto/PagingParams.java b/src/main/java/ru/veselov/companybot/dto/PagingParams.java new file mode 100644 index 0000000..8ab7116 --- /dev/null +++ b/src/main/java/ru/veselov/companybot/dto/PagingParams.java @@ -0,0 +1,17 @@ +package ru.veselov.companybot.dto; + +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.PositiveOrZero; +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class PagingParams { + + @PositiveOrZero + private Integer page; + + @Positive + private Integer size; +} diff --git a/src/main/java/ru/veselov/companybot/mapper/InquiryMapper.java b/src/main/java/ru/veselov/companybot/mapper/InquiryMapper.java index 3661c36..6821b20 100644 --- a/src/main/java/ru/veselov/companybot/mapper/InquiryMapper.java +++ b/src/main/java/ru/veselov/companybot/mapper/InquiryMapper.java @@ -2,17 +2,18 @@ import org.mapstruct.Mapper; import org.mapstruct.ReportingPolicy; +import org.springframework.data.domain.Page; import ru.veselov.companybot.dto.InquiryResponseDTO; import ru.veselov.companybot.entity.InquiryEntity; -import java.util.List; - @Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE, uses = {CustomerMapper.class, MessageMapper.class}) public interface InquiryMapper { InquiryResponseDTO entityToDTO(InquiryEntity inquiryEntity); - List entitiesToDTOS(List entities); + default Page entitiesToDTOS(Page entities) { + return entities.map(this::entityToDTO); + } } diff --git a/src/main/java/ru/veselov/companybot/model/DivisionModel.java b/src/main/java/ru/veselov/companybot/model/DivisionModel.java index a18e489..08ca303 100644 --- a/src/main/java/ru/veselov/companybot/model/DivisionModel.java +++ b/src/main/java/ru/veselov/companybot/model/DivisionModel.java @@ -20,13 +20,13 @@ @Builder public class DivisionModel implements Serializable { - @Schema(description = "Id of division", example = "3b4cb719-3489-445d-bb01-ef7958aca896") + @Schema(description = "Id отдела", example = "3b4cb719-3489-445d-bb01-ef7958aca896") private UUID divisionId; - @Schema(description = "Short name of division", example = "Common") + @Schema(description = "Короткое наименование отдела", example = "Common") private String name; - @Schema(description = "Description of division", example = "Common questions here") + @Schema(description = "Описание отдела", example = "Common questions here") private String description; @JsonIgnore diff --git a/src/main/java/ru/veselov/companybot/service/InquiryService.java b/src/main/java/ru/veselov/companybot/service/InquiryService.java index d109d66..10139c6 100644 --- a/src/main/java/ru/veselov/companybot/service/InquiryService.java +++ b/src/main/java/ru/veselov/companybot/service/InquiryService.java @@ -1,6 +1,8 @@ package ru.veselov.companybot.service; +import org.springframework.data.domain.Page; import ru.veselov.companybot.dto.InquiryResponseDTO; +import ru.veselov.companybot.dto.PagingParams; import ru.veselov.companybot.model.InquiryModel; import java.util.List; @@ -9,6 +11,6 @@ public interface InquiryService { InquiryResponseDTO save(InquiryModel inquiry); - List findAll(); + Page findAll(PagingParams pagingParams); } diff --git a/src/main/java/ru/veselov/companybot/service/impl/InquiryServiceImpl.java b/src/main/java/ru/veselov/companybot/service/impl/InquiryServiceImpl.java index 837b51c..295c8b0 100644 --- a/src/main/java/ru/veselov/companybot/service/impl/InquiryServiceImpl.java +++ b/src/main/java/ru/veselov/companybot/service/impl/InquiryServiceImpl.java @@ -2,11 +2,15 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.telegram.telegrambots.meta.api.objects.Message; import ru.veselov.companybot.bot.util.BotUtils; import ru.veselov.companybot.dto.InquiryResponseDTO; +import ru.veselov.companybot.dto.PagingParams; import ru.veselov.companybot.entity.CustomerEntity; import ru.veselov.companybot.entity.CustomerMessageEntity; import ru.veselov.companybot.entity.DivisionEntity; @@ -19,7 +23,6 @@ import ru.veselov.companybot.service.InquiryService; import ru.veselov.companybot.util.LogMessageUtils; -import java.util.List; import java.util.Optional; import java.util.UUID; @@ -71,8 +74,9 @@ public InquiryResponseDTO save(InquiryModel inquiry) { } @Override - public List findAll() { - List inquiryResponseDTOS = inquiryMapper.entitiesToDTOS(inquiryRepository.findAll()); + public Page findAll(PagingParams pagingParams) { + Pageable pageable = PageRequest.of(pagingParams.getPage(), pagingParams.getSize()); + Page inquiryResponseDTOS = inquiryMapper.entitiesToDTOS(inquiryRepository.findAll(pageable)); log.debug("Retrieved inquiries from DB"); return inquiryResponseDTOS; } diff --git a/src/test/java/ru/veselov/companybot/it/controller/InquiryControllerIntegrationTest.java b/src/test/java/ru/veselov/companybot/it/controller/InquiryControllerIntegrationTest.java new file mode 100644 index 0000000..d2e8690 --- /dev/null +++ b/src/test/java/ru/veselov/companybot/it/controller/InquiryControllerIntegrationTest.java @@ -0,0 +1,25 @@ +package ru.veselov.companybot.it.controller; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import ru.veselov.companybot.config.BotMocks; +import ru.veselov.companybot.config.EnableTestContainers; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@AutoConfigureMockMvc +@DirtiesContext +@Import({BotMocks.class}) +@ActiveProfiles("test") +@EnableTestContainers +class InquiryControllerIntegrationTest { + + @Autowired + MockMvc mockMvc; + + +} diff --git a/src/test/java/ru/veselov/companybot/service/impl/InquiryServiceImplTest.java b/src/test/java/ru/veselov/companybot/service/impl/InquiryServiceImplTest.java index 7ff68d7..2b0a081 100644 --- a/src/test/java/ru/veselov/companybot/service/impl/InquiryServiceImplTest.java +++ b/src/test/java/ru/veselov/companybot/service/impl/InquiryServiceImplTest.java @@ -10,10 +10,14 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; import org.springframework.test.util.ReflectionTestUtils; import ru.veselov.companybot.bot.util.BotUtils; import ru.veselov.companybot.dto.CustomerResponseDTO; import ru.veselov.companybot.dto.InquiryResponseDTO; +import ru.veselov.companybot.dto.PagingParams; import ru.veselov.companybot.entity.CustomerEntity; import ru.veselov.companybot.entity.CustomerMessageEntity; import ru.veselov.companybot.entity.DivisionEntity; @@ -66,13 +70,17 @@ void findAll_AllOk_ReturnDTOs() { CustomerEntity customerEntity = TestUtils.getCustomerEntity(); customerEntity.setContacts(Set.of(TestUtils.getContactEntity())); inquiryEntity.setCustomer(customerEntity); - Mockito.when(inquiryRepository.findAll()).thenReturn(List.of(inquiryEntity)); + PagingParams pagingParams = new PagingParams(0, 100); + Page page = new PageImpl<>(List.of(inquiryEntity), + PageRequest.of(pagingParams.getPage(), pagingParams.getSize()), 100); + Mockito.when(inquiryRepository.findAll(Mockito.any(PageRequest.class))).thenReturn(page); - List inquiries = inquiryService.findAll(); + Page inquiries = inquiryService.findAll(pagingParams); - Assertions.assertThat(inquiries).hasSize(1).extracting(InquiryResponseDTO::getInquiryId).doesNotContainNull() + List content = inquiries.getContent(); + Assertions.assertThat(content).hasSize(1).extracting(InquiryResponseDTO::getInquiryId).doesNotContainNull() .containsExactly(inquiryEntity.getInquiryId()); - InquiryResponseDTO inquiryResponseDTO = inquiries.get(0); + InquiryResponseDTO inquiryResponseDTO = content.get(0); Assertions.assertThat(inquiryResponseDTO.getDivision()) .extracting(DivisionModel::getDivisionId, DivisionModel::getName, DivisionModel::getDescription) @@ -82,10 +90,9 @@ void findAll_AllOk_ReturnDTOs() { CustomerResponseDTO::getLastName, CustomerResponseDTO::getUserName) .containsExactly(customerEntity.getId(), customerEntity.getFirstName(), customerEntity.getLastName(), customerEntity.getUserName()); - Mockito.verify(inquiryRepository).findAll(); + Mockito.verify(inquiryRepository).findAll(Mockito.any(PageRequest.class)); } - @Test void save_CustomerDivisionFound_SaveAndReturn() { CustomerEntity customerEntity = TestUtils.getCustomerEntity();