diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/config/DataConfiguration.java b/data-runtime/src/main/java/io/micronaut/data/runtime/config/DataConfiguration.java index 2202190711..2b167ff755 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/config/DataConfiguration.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/config/DataConfiguration.java @@ -36,6 +36,7 @@ public class DataConfiguration implements DataSettings { public static class PageableConfiguration { public static final int DEFAULT_MAX_PAGE_SIZE = 100; public static final boolean DEFAULT_SORT_IGNORE_CASE = false; + public static final boolean DEFAULT_START_FROM_PAGE_ONE = false; public static final String DEFAULT_SORT_PARAMETER = "sort"; public static final String DEFAULT_SIZE_PARAMETER = "size"; public static final String DEFAULT_PAGE_PARAMETER = "page"; @@ -43,6 +44,7 @@ public static class PageableConfiguration { private int maxPageSize = DEFAULT_MAX_PAGE_SIZE; private Integer defaultPageSize = null; // When is not specified the maxPageSize should be used private boolean sortIgnoreCase = DEFAULT_SORT_IGNORE_CASE; + private boolean startFromPageOne = DEFAULT_START_FROM_PAGE_ONE; private String sortParameterName = DEFAULT_SORT_PARAMETER; private String sizeParameterName = DEFAULT_SIZE_PARAMETER; private String pageParameterName = DEFAULT_PAGE_PARAMETER; @@ -78,6 +80,23 @@ public void setSortDelimiter(String sortDelimiter) { } } + /** + * @return Whether the first page parameter starts at one + */ + public boolean isStartFromPageOne() { + return startFromPageOne; + } + + /** + * This parameter is used to shift the provided page number back one when this is true. + * So when the page parameter is entered as one, the first page (0) is provided. + * + * @param startFromPageOne Whether the first page is 1 + */ + public void setStartFromPageOne(boolean startFromPageOne) { + this.startFromPageOne = startFromPageOne; + } + /** * @return The maximum page size when binding {@link io.micronaut.data.model.Pageable} objects. */ @@ -95,7 +114,7 @@ public void setMaxPageSize(int maxPageSize) { /** * @return the page size to use when binding {@link io.micronaut.data.model.Pageable} - * objects and no size parameter is used. By default is set to the same vale as {@link #maxPageSize} + * objects and no size parameter is used. By default is set to the same value as {@link #maxPageSize} */ public int getDefaultPageSize() { return defaultPageSize == null ? maxPageSize : defaultPageSize; diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/http/PageableRequestArgumentBinder.java b/data-runtime/src/main/java/io/micronaut/data/runtime/http/PageableRequestArgumentBinder.java index 8660e6c4af..10f7a0425c 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/http/PageableRequestArgumentBinder.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/http/PageableRequestArgumentBinder.java @@ -26,6 +26,7 @@ import io.micronaut.http.bind.binders.RequestArgumentBinder; import io.micronaut.http.bind.binders.TypedRequestArgumentBinder; +import io.micronaut.http.exceptions.HttpStatusException; import jakarta.inject.Singleton; import java.util.List; import java.util.Locale; @@ -96,6 +97,13 @@ public BindingResult bind(ArgumentConversionContext context, sort = Sort.of(orders); } + if (configuration.isStartFromPageOne()) { + if (page < 1) { + throw new IllegalArgumentException(String.format("%s parameter starts at 1", configuration.getPageParameterName())); + } + page--; + } + if (size < 1) { if (page == 0 && configuredMaxSize < 1 && sort == null) { pageable = Pageable.UNPAGED; diff --git a/data-runtime/src/test/groovy/io/micronaut/data/runtime/http/PageableConfigSpec.groovy b/data-runtime/src/test/groovy/io/micronaut/data/runtime/http/PageableConfigSpec.groovy index 622632684a..255979c98a 100644 --- a/data-runtime/src/test/groovy/io/micronaut/data/runtime/http/PageableConfigSpec.groovy +++ b/data-runtime/src/test/groovy/io/micronaut/data/runtime/http/PageableConfigSpec.groovy @@ -28,7 +28,8 @@ class PageableConfigSpec extends Specification { 'micronaut.data.pageable.default-page-size': 10, 'micronaut.data.pageable.sort-parameter-name': 's', 'micronaut.data.pageable.page-parameter-name': 'index', - 'micronaut.data.pageable.size-parameter-name': 'max' + 'micronaut.data.pageable.size-parameter-name': 'max', + 'micronaut.data.pageable.start-from-page-one': true ) DataConfiguration.PageableConfiguration configuration = context.getBean(DataConfiguration.PageableConfiguration) @@ -38,6 +39,7 @@ class PageableConfigSpec extends Specification { configuration.sortParameterName == 's' configuration.pageParameterName == 'index' configuration.sizeParameterName == 'max' + configuration.startFromPageOne cleanup: context.close() diff --git a/data-runtime/src/test/groovy/io/micronaut/data/runtime/http/PageableRequestArgumentBinderSpec.groovy b/data-runtime/src/test/groovy/io/micronaut/data/runtime/http/PageableRequestArgumentBinderSpec.groovy index f78df6c814..401bde1068 100644 --- a/data-runtime/src/test/groovy/io/micronaut/data/runtime/http/PageableRequestArgumentBinderSpec.groovy +++ b/data-runtime/src/test/groovy/io/micronaut/data/runtime/http/PageableRequestArgumentBinderSpec.groovy @@ -72,10 +72,10 @@ class PageableRequestArgumentBinderSpec extends Specification { void 'test bind size #size and page #page with custom configuration'() { given: def configuration = new DataConfiguration.PageableConfiguration() - configuration.defaultPageSize=40 - configuration.maxPageSize=200 - configuration.sizeParameterName="perPage" - configuration.pageParameterName="myPage" + configuration.defaultPageSize = 40 + configuration.maxPageSize = 200 + configuration.sizeParameterName = "perPage" + configuration.pageParameterName = "myPage" PageableRequestArgumentBinder binder = new PageableRequestArgumentBinder(configuration) def get = HttpRequest.GET('/') get.parameters.add("perPage", size) @@ -94,4 +94,38 @@ class PageableRequestArgumentBinderSpec extends Specification { "-1" | "0" | 40 | 0 // negative => uses default != max "junk" | "0" | 40 | 0 // can't be parsed } + + @Unroll + void 'test page #page is or is not shifted with custom configuration for startFromPageOne #startFromPageOne'() { + given: + def configuration = new DataConfiguration.PageableConfiguration() + configuration.startFromPageOne = startFromPageOne + PageableRequestArgumentBinder binder = new PageableRequestArgumentBinder(configuration) + def get = HttpRequest.GET('/') + get.parameters.add("page", page) + Pageable p = binder.bind(ConversionContext.of(Pageable), get).get() + + expect: + p.number == pageNumber + + where: + page | pageNumber | startFromPageOne + "1" | 0 | true // first page is shifted to 0 + "5" | 4 | true // fifth page is shifted to 4 + } + + void 'test IllegalArgumentException is thrown when startFromPageOne is true and page provided is 0'() { + given: + def configuration = new DataConfiguration.PageableConfiguration() + configuration.startFromPageOne = true + PageableRequestArgumentBinder binder = new PageableRequestArgumentBinder(configuration) + def get = HttpRequest.GET('/') + get.parameters.add("page", "0") + + when: + binder.bind(ConversionContext.of(Pageable), get).get() + + then: + thrown(IllegalArgumentException) + } } diff --git a/data-spring/src/main/java/io/micronaut/data/spring/runtime/http/SpringPageableRequestArgumentBinder.java b/data-spring/src/main/java/io/micronaut/data/spring/runtime/http/SpringPageableRequestArgumentBinder.java index e4008c9a28..6cfde2478a 100644 --- a/data-spring/src/main/java/io/micronaut/data/spring/runtime/http/SpringPageableRequestArgumentBinder.java +++ b/data-spring/src/main/java/io/micronaut/data/spring/runtime/http/SpringPageableRequestArgumentBinder.java @@ -100,6 +100,13 @@ public BindingResult bind(ArgumentConversionContext context, sort = Sort.unsorted(); } + if (configuration.isStartFromPageOne()) { + if (page < 1) { + throw new IllegalArgumentException(String.format("%s parameter starts at 1", configuration.getPageParameterName())); + } + page--; + } + if (size < 1) { if (page == 0 && configuredMaxSize < 1 && sort.isUnsorted()) { pageable = Pageable.unpaged();