diff --git a/spring-reactive-modules/pom.xml b/spring-reactive-modules/pom.xml index c8c9c84394..e75682da78 100644 --- a/spring-reactive-modules/pom.xml +++ b/spring-reactive-modules/pom.xml @@ -18,6 +18,7 @@ spring-5-data-reactive + spring-5-data-reactive-2 spring-5-reactive spring-5-reactive-2 spring-5-reactive-3 diff --git a/spring-reactive-modules/spring-5-data-reactive-2/README.md b/spring-reactive-modules/spring-5-data-reactive-2/README.md new file mode 100644 index 0000000000..d12e8214cd --- /dev/null +++ b/spring-reactive-modules/spring-5-data-reactive-2/README.md @@ -0,0 +1,9 @@ +## Spring Data Reactive Project + +This module contains articles about reactive Spring 5 Data + +### The Course + +The "REST With Spring" Classes: http://bit.ly/restwithspring + +### Relevant Articles diff --git a/spring-reactive-modules/spring-5-data-reactive-2/pom.xml b/spring-reactive-modules/spring-5-data-reactive-2/pom.xml new file mode 100644 index 0000000000..e5447ac038 --- /dev/null +++ b/spring-reactive-modules/spring-5-data-reactive-2/pom.xml @@ -0,0 +1,77 @@ + + + 4.0.0 + spring-5-data-reactive-2 + spring-5-data-reactive-2 + jar + + + com.baeldung.spring.reactive + spring-reactive-modules + 1.0.0-SNAPSHOT + + + + + 8 + 8 + UTF-8 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springframework.boot + spring-boot-starter-data-r2dbc + + + org.springframework + spring-webflux + + + org.springframework.boot + spring-boot-starter-test + test + + + com.h2database + h2 + runtime + + + io.r2dbc + r2dbc-h2 + runtime + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-test + test + + + io.projectreactor + reactor-test + test + + + javax.validation + validation-api + 2.0.1.Final + + + + \ No newline at end of file diff --git a/spring-reactive-modules/spring-5-data-reactive-2/src/main/java/com/baeldung/pagination/PaginationApplication.java b/spring-reactive-modules/spring-5-data-reactive-2/src/main/java/com/baeldung/pagination/PaginationApplication.java new file mode 100644 index 0000000000..799c73cfb7 --- /dev/null +++ b/spring-reactive-modules/spring-5-data-reactive-2/src/main/java/com/baeldung/pagination/PaginationApplication.java @@ -0,0 +1,13 @@ +package com.baeldung.pagination; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class PaginationApplication { + + public static void main(String[] args) { + SpringApplication.run(PaginationApplication.class, args); + } + +} diff --git a/spring-reactive-modules/spring-5-data-reactive-2/src/main/java/com/baeldung/pagination/config/CustomWebMvcConfigurationSupport.java b/spring-reactive-modules/spring-5-data-reactive-2/src/main/java/com/baeldung/pagination/config/CustomWebMvcConfigurationSupport.java new file mode 100644 index 0000000000..c8f48ee202 --- /dev/null +++ b/spring-reactive-modules/spring-5-data-reactive-2/src/main/java/com/baeldung/pagination/config/CustomWebMvcConfigurationSupport.java @@ -0,0 +1,32 @@ +package com.baeldung.pagination.config; + +import java.util.List; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.web.PageableHandlerMethodArgumentResolver; +import org.springframework.data.web.SortHandlerMethodArgumentResolver; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport; + +@Configuration +public class CustomWebMvcConfigurationSupport extends WebMvcConfigurationSupport { + + @Bean + public PageRequest defaultPageRequest() { + return PageRequest.of(0, 100); + } + + @Override + protected void addArgumentResolvers(List argumentResolvers) { + SortHandlerMethodArgumentResolver argumentResolver = new SortHandlerMethodArgumentResolver(); + argumentResolver.setSortParameter("sort"); + PageableHandlerMethodArgumentResolver resolver = new PageableHandlerMethodArgumentResolver(argumentResolver); + resolver.setFallbackPageable(defaultPageRequest()); + resolver.setPageParameterName("page"); + resolver.setSizeParameterName("size"); + argumentResolvers.add(resolver); + } + +} diff --git a/spring-reactive-modules/spring-5-data-reactive-2/src/main/java/com/baeldung/pagination/config/DatabaseConfig.java b/spring-reactive-modules/spring-5-data-reactive-2/src/main/java/com/baeldung/pagination/config/DatabaseConfig.java new file mode 100644 index 0000000000..10a30f9c7a --- /dev/null +++ b/spring-reactive-modules/spring-5-data-reactive-2/src/main/java/com/baeldung/pagination/config/DatabaseConfig.java @@ -0,0 +1,29 @@ +package com.baeldung.pagination.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ClassPathResource; +import org.springframework.r2dbc.connection.init.CompositeDatabasePopulator; +import org.springframework.r2dbc.connection.init.ConnectionFactoryInitializer; +import org.springframework.r2dbc.connection.init.ResourceDatabasePopulator; + +import io.r2dbc.spi.ConnectionFactory; + + +@Configuration +public class DatabaseConfig { + + @Bean + public ConnectionFactoryInitializer initializer(ConnectionFactory connectionFactory) { + + ConnectionFactoryInitializer initializer = new ConnectionFactoryInitializer(); + initializer.setConnectionFactory(connectionFactory); + + CompositeDatabasePopulator populator = new CompositeDatabasePopulator(); + populator.addPopulators(new ResourceDatabasePopulator(new ClassPathResource("init.sql"))); + initializer.setDatabasePopulator(populator); + + return initializer; + } + +} diff --git a/spring-reactive-modules/spring-5-data-reactive-2/src/main/java/com/baeldung/pagination/controller/ProductPaginationController.java b/spring-reactive-modules/spring-5-data-reactive-2/src/main/java/com/baeldung/pagination/controller/ProductPaginationController.java new file mode 100644 index 0000000000..077980ecf3 --- /dev/null +++ b/spring-reactive-modules/spring-5-data-reactive-2/src/main/java/com/baeldung/pagination/controller/ProductPaginationController.java @@ -0,0 +1,29 @@ +package com.baeldung.pagination.controller; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.baeldung.pagination.model.Product; +import com.baeldung.pagination.repository.ProductRepository; + +import lombok.RequiredArgsConstructor; +import reactor.core.publisher.Mono; + +@RestController +@RequiredArgsConstructor +public class ProductPaginationController { + + private final ProductRepository productRepository; + + @GetMapping("/products") + public Mono> findAllProducts(Pageable pageable) { + return this.productRepository.findAllBy(pageable) + .collectList() + .zipWith(this.productRepository.count()) + .map(p -> new PageImpl<>(p.getT1(), pageable, p.getT2())); + } + +} diff --git a/spring-reactive-modules/spring-5-data-reactive-2/src/main/java/com/baeldung/pagination/model/Product.java b/spring-reactive-modules/spring-5-data-reactive-2/src/main/java/com/baeldung/pagination/model/Product.java new file mode 100644 index 0000000000..c82e31309c --- /dev/null +++ b/spring-reactive-modules/spring-5-data-reactive-2/src/main/java/com/baeldung/pagination/model/Product.java @@ -0,0 +1,32 @@ +package com.baeldung.pagination.model; + +import java.util.UUID; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +import org.springframework.data.annotation.Id; +import org.springframework.data.relational.core.mapping.Table; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Table +public class Product { + + @Id + @Getter + private UUID id; + + @NotNull + @Size(max = 255, message = "The property 'name' must be less than or equal to 255 characters.") + private String name; + + @NotNull + private double price; +} diff --git a/spring-reactive-modules/spring-5-data-reactive-2/src/main/java/com/baeldung/pagination/repository/ProductRepository.java b/spring-reactive-modules/spring-5-data-reactive-2/src/main/java/com/baeldung/pagination/repository/ProductRepository.java new file mode 100644 index 0000000000..1610d452da --- /dev/null +++ b/spring-reactive-modules/spring-5-data-reactive-2/src/main/java/com/baeldung/pagination/repository/ProductRepository.java @@ -0,0 +1,16 @@ +package com.baeldung.pagination.repository; + +import java.util.UUID; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.repository.reactive.ReactiveSortingRepository; +import org.springframework.stereotype.Repository; + +import com.baeldung.pagination.model.Product; + +import reactor.core.publisher.Flux; + +@Repository +public interface ProductRepository extends ReactiveSortingRepository { + Flux findAllBy(Pageable pageable); +} diff --git a/spring-reactive-modules/spring-5-data-reactive-2/src/main/resources/application.properties b/spring-reactive-modules/spring-5-data-reactive-2/src/main/resources/application.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/spring-reactive-modules/spring-5-data-reactive-2/src/main/resources/init.sql b/spring-reactive-modules/spring-5-data-reactive-2/src/main/resources/init.sql new file mode 100644 index 0000000000..043228d3cd --- /dev/null +++ b/spring-reactive-modules/spring-5-data-reactive-2/src/main/resources/init.sql @@ -0,0 +1,15 @@ +create table product +( + id UUID DEFAULT RANDOM_UUID() PRIMARY KEY, + name varchar(50), + price decimal +); + +insert into product(name, price) +values ('product_A', 1.0); +insert into product(name, price) +values ('product_B', 2.0); +insert into product(name, price) +values ('product_C', 3.0); +insert into product(name, price) +values ('product_D', 4.0); diff --git a/spring-reactive-modules/spring-5-data-reactive-2/src/test/java/com/baeldung/pagination/controller/ProductPaginationControllerIntegrationTest.java b/spring-reactive-modules/spring-5-data-reactive-2/src/test/java/com/baeldung/pagination/controller/ProductPaginationControllerIntegrationTest.java new file mode 100644 index 0000000000..a0af1f59ca --- /dev/null +++ b/spring-reactive-modules/spring-5-data-reactive-2/src/test/java/com/baeldung/pagination/controller/ProductPaginationControllerIntegrationTest.java @@ -0,0 +1,122 @@ +package com.baeldung.pagination.controller; + +import static org.assertj.core.api.Assertions.atIndex; +import static org.assertj.core.api.AssertionsForClassTypes.tuple; +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +import java.util.List; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.web.reactive.server.WebTestClient; + +import com.baeldung.pagination.model.Product; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@AutoConfigureWebTestClient +class ProductPaginationControllerIntegrationTest { + + @Autowired + private WebTestClient webClient; + + @Test + void WhenProductEndpointIsHit_thenShouldReturnProductsWithPagination() throws JsonProcessingException { + String response = webClient.get() + .uri("/products") + .exchange() + .expectStatus() + .is2xxSuccessful() + .expectBody(String.class) + .returnResult() + .getResponseBody(); + Assertions.assertNotNull(response); + + JsonNode pageResponse = new ObjectMapper().readValue(response, JsonNode.class); + Assertions.assertNotNull(pageResponse); + Assertions.assertEquals(4, pageResponse.get("totalElements") + .asInt()); + Assertions.assertEquals(1, pageResponse.get("totalPages") + .asInt()); + Assertions.assertTrue(pageResponse.get("last") + .asBoolean()); + Assertions.assertTrue(pageResponse.get("first") + .asBoolean()); + Assertions.assertEquals(100, pageResponse.get("size") + .asInt()); + Assertions.assertEquals(4, pageResponse.get("numberOfElements") + .asInt()); + Assertions.assertEquals(0, pageResponse.get("pageable") + .get("offset") + .asInt()); + Assertions.assertEquals(0, pageResponse.get("pageable") + .get("pageNumber") + .asInt()); + Assertions.assertEquals(100, pageResponse.get("pageable") + .get("pageSize") + .asInt()); + Assertions.assertTrue(pageResponse.get("pageable") + .get("paged") + .asBoolean()); + List content = new ObjectMapper().readValue(String.valueOf(pageResponse.get("content")), new TypeReference>() { + }); + assertThat(content).hasSize(4); + assertThat(content).extracting("name", "price") + .contains(tuple("product_A", 1.0), atIndex(0)) + .contains(tuple("product_B", 2.0), atIndex(1)) + .contains(tuple("product_C", 3.0), atIndex(2)) + .contains(tuple("product_D", 4.0), atIndex(3)); + } + + @Test + void WhenProductEndpointIsHitWithPageSizeAs2AndSortPriceByDesc_thenShouldReturnProductsWithPaginationIgnoring2Products() throws JsonProcessingException { + String response = webClient.get() + .uri("/products?page=1&size=2&sort=price,DESC") + .exchange() + .expectStatus() + .is2xxSuccessful() + .expectBody(String.class) + .returnResult() + .getResponseBody(); + Assertions.assertNotNull(response); + + JsonNode pageResponse = new ObjectMapper().readValue(response, JsonNode.class); + Assertions.assertNotNull(pageResponse); + Assertions.assertEquals(4, pageResponse.get("totalElements") + .asInt()); + Assertions.assertEquals(2, pageResponse.get("totalPages") + .asInt()); + Assertions.assertTrue(pageResponse.get("last") + .asBoolean()); + Assertions.assertFalse(pageResponse.get("first") + .asBoolean()); + Assertions.assertEquals(2, pageResponse.get("size") + .asInt()); + Assertions.assertEquals(2, pageResponse.get("numberOfElements") + .asInt()); + Assertions.assertEquals(2, pageResponse.get("pageable") + .get("offset") + .asInt()); + Assertions.assertEquals(1, pageResponse.get("pageable") + .get("pageNumber") + .asInt()); + Assertions.assertEquals(2, pageResponse.get("pageable") + .get("pageSize") + .asInt()); + Assertions.assertTrue(pageResponse.get("pageable") + .get("paged") + .asBoolean()); + List content = new ObjectMapper().readValue(String.valueOf(pageResponse.get("content")), new TypeReference>() { + }); + assertThat(content).hasSize(2); + assertThat(content).extracting("name", "price") + .contains(tuple("product_B", 2.0), atIndex(0)) + .contains(tuple("product_A", 1.0), atIndex(1)); + } +} \ No newline at end of file