From 4718c454ae04a66679ef159f0ae2d2bbaec9c8b4 Mon Sep 17 00:00:00 2001 From: Mark Paluch <mark.paluch@broadcom.com> Date: Thu, 21 Nov 2024 08:56:32 +0100 Subject: [PATCH] Introduce public `ReactivePageableExecutionUtils` variant of pageable utils. Closes #3209 --- .../ReactivePageableExecutionUtils.java | 69 +++++++++++ ...activePageableExecutionUtilsUnitTests.java | 112 ++++++++++++++++++ 2 files changed, 181 insertions(+) create mode 100644 src/main/java/org/springframework/data/support/ReactivePageableExecutionUtils.java create mode 100755 src/test/java/org/springframework/data/support/ReactivePageableExecutionUtilsUnitTests.java diff --git a/src/main/java/org/springframework/data/support/ReactivePageableExecutionUtils.java b/src/main/java/org/springframework/data/support/ReactivePageableExecutionUtils.java new file mode 100644 index 0000000000..06f54950ac --- /dev/null +++ b/src/main/java/org/springframework/data/support/ReactivePageableExecutionUtils.java @@ -0,0 +1,69 @@ +/* + * Copyright 2021-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.support; + +import reactor.core.publisher.Mono; + +import java.util.List; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.util.Assert; + +/** + * Support for query execution using {@link Pageable}. Using {@link ReactivePageableExecutionUtils} assumes that data + * queries are cheaper than {@code COUNT} queries and so some cases can take advantage of optimizations. + * + * @author Mark Paluch + * @since 3.5 + */ +public abstract class ReactivePageableExecutionUtils { + + private ReactivePageableExecutionUtils() {} + + /** + * Constructs a {@link Page} based on the given {@code content}, {@link Pageable} and {@link Mono} applying + * optimizations. The construction of {@link Page} omits a count query if the total can be determined based on the + * result size and {@link Pageable}. + * + * @param content must not be {@literal null}. + * @param pageable must not be {@literal null}. + * @param totalSupplier must not be {@literal null}. + * @return the {@link Page}. + */ + public static <T> Mono<Page<T>> getPage(List<T> content, Pageable pageable, Mono<Long> totalSupplier) { + + Assert.notNull(content, "Content must not be null"); + Assert.notNull(pageable, "Pageable must not be null"); + Assert.notNull(totalSupplier, "TotalSupplier must not be null"); + + if (pageable.isUnpaged() || pageable.getOffset() == 0) { + + if (pageable.isUnpaged() || pageable.getPageSize() > content.size()) { + return Mono.just(new PageImpl<>(content, pageable, content.size())); + } + + return totalSupplier.map(total -> new PageImpl<>(content, pageable, total)); + } + + if (!content.isEmpty() && pageable.getPageSize() > content.size()) { + return Mono.just(new PageImpl<>(content, pageable, pageable.getOffset() + content.size())); + } + + return totalSupplier.map(total -> new PageImpl<>(content, pageable, total)); + } +} diff --git a/src/test/java/org/springframework/data/support/ReactivePageableExecutionUtilsUnitTests.java b/src/test/java/org/springframework/data/support/ReactivePageableExecutionUtilsUnitTests.java new file mode 100755 index 0000000000..5d4c147c73 --- /dev/null +++ b/src/test/java/org/springframework/data/support/ReactivePageableExecutionUtilsUnitTests.java @@ -0,0 +1,112 @@ +/* + * Copyright 2020-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.support; + +import static java.util.Collections.*; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import reactor.core.publisher.Mono; + +import java.util.Arrays; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; + +/** + * Unit tests for {@link ReactivePageableExecutionUtils}. + * + * @author Mark Paluch + */ +@SuppressWarnings("unchecked") +@ExtendWith(MockitoExtension.class) +class ReactivePageableExecutionUtilsUnitTests { + + @Test // DATAMCNS-884, GH-3209 + void firstPageRequestIsLessThanOneFullPageDoesNotRequireTotal() { + + Mono<Long> totalSupplierMock = mock(Mono.class); + var page = ReactivePageableExecutionUtils.getPage(Arrays.asList(1, 2, 3), PageRequest.of(0, 10), totalSupplierMock) + .block(); + + assertThat(page).contains(1, 2, 3); + assertThat(page.getTotalElements()).isEqualTo(3L); + verifyNoInteractions(totalSupplierMock); + } + + @Test // DATAMCNS-884, GH-3209 + void noPageableRequestDoesNotRequireTotal() { + + Mono<Long> totalSupplierMock = mock(Mono.class); + var page = ReactivePageableExecutionUtils.getPage(Arrays.asList(1, 2, 3), Pageable.unpaged(), totalSupplierMock) + .block(); + + assertThat(page).contains(1, 2, 3); + assertThat(page.getTotalElements()).isEqualTo(3L); + + verifyNoInteractions(totalSupplierMock); + } + + @Test // DATAMCNS-884, GH-3209 + void subsequentPageRequestIsLessThanOneFullPageDoesNotRequireTotal() { + + Mono<Long> totalSupplierMock = mock(Mono.class); + var page = ReactivePageableExecutionUtils.getPage(Arrays.asList(1, 2, 3), PageRequest.of(5, 10), totalSupplierMock) + .block(); + + assertThat(page).contains(1, 2, 3); + assertThat(page.getTotalElements()).isEqualTo(53L); + + verifyNoInteractions(totalSupplierMock); + } + + @Test // DATAMCNS-884, GH-3209 + void firstPageRequestHitsUpperBoundRequiresTotal() { + + Mono<Long> totalSupplierMock = Mono.just(4L); + + var page = ReactivePageableExecutionUtils.getPage(Arrays.asList(1, 2, 3), PageRequest.of(0, 3), totalSupplierMock) + .block(); + + assertThat(page).contains(1, 2, 3); + assertThat(page.getTotalElements()).isEqualTo(4L); + } + + @Test // DATAMCNS-884, GH-3209 + void subsequentPageRequestHitsUpperBoundRequiresTotal() { + + Mono<Long> totalSupplierMock = Mono.just(7L); + + var page = ReactivePageableExecutionUtils.getPage(Arrays.asList(1, 2, 3), PageRequest.of(1, 3), totalSupplierMock) + .block(); + + assertThat(page).contains(1, 2, 3); + assertThat(page.getTotalElements()).isEqualTo(7L); + } + + @Test // DATAMCNS-884, GH-3209 + void subsequentPageRequestWithoutResultRequiresRequireTotal() { + + Mono<Long> totalSupplierMock = Mono.just(7L); + var page = ReactivePageableExecutionUtils.getPage(emptyList(), PageRequest.of(5, 10), totalSupplierMock).block(); + + assertThat(page.getTotalElements()).isEqualTo(7L); + } +}