diff --git a/src/main/java/org/springframework/data/web/OffsetScrollPositionArgumentResolver.java b/src/main/java/org/springframework/data/web/OffsetScrollPositionArgumentResolver.java new file mode 100644 index 0000000000..51338b0ef4 --- /dev/null +++ b/src/main/java/org/springframework/data/web/OffsetScrollPositionArgumentResolver.java @@ -0,0 +1,53 @@ +/* + * Copyright 2016-2023 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.web; + +import org.springframework.core.MethodParameter; +import org.springframework.data.domain.OffsetScrollPosition; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +/** + * Argument resolver to extract a {@link OffsetScrollPosition} object from a {@link NativeWebRequest} for a particular + * {@link MethodParameter}. A {@link OffsetScrollPositionArgumentResolver} can either resolve {@link OffsetScrollPosition} itself or wrap another + * {@link OffsetScrollPositionArgumentResolver} to post-process {@link OffsetScrollPosition}. + * + * @since 3.2 + * @author Yanming Zhou + * @see HandlerMethodArgumentResolver + */ +public interface OffsetScrollPositionArgumentResolver extends HandlerMethodArgumentResolver { + + /** + * Resolves a {@link OffsetScrollPosition} method parameter into an argument value from a given request. + * + * @param parameter the method parameter to resolve. This parameter must have previously been passed to + * {@link #supportsParameter} which must have returned {@code true}. + * @param mavContainer the ModelAndViewContainer for the current request + * @param webRequest the current request + * @param binderFactory a factory for creating {@link WebDataBinder} instances + * @return the resolved argument value + */ + @NonNull + @Override + OffsetScrollPosition resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory); +} diff --git a/src/main/java/org/springframework/data/web/OffsetScrollPositionHandlerMethodArgumentResolver.java b/src/main/java/org/springframework/data/web/OffsetScrollPositionHandlerMethodArgumentResolver.java new file mode 100644 index 0000000000..8dea7d95d9 --- /dev/null +++ b/src/main/java/org/springframework/data/web/OffsetScrollPositionHandlerMethodArgumentResolver.java @@ -0,0 +1,49 @@ +/* + * Copyright 2013-2023 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.web; + +import org.springframework.core.MethodParameter; +import org.springframework.data.domain.OffsetScrollPosition; +import org.springframework.lang.Nullable; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +import java.util.Arrays; + +/** + * {@link HandlerMethodArgumentResolver} to automatically create {@link OffsetScrollPosition} instances from request parameters. + * + * @since 3.2 + * @author Yanming Zhou + */ +public class OffsetScrollPositionHandlerMethodArgumentResolver extends OffsetScrollPositionHandlerMethodArgumentResolverSupport + implements OffsetScrollPositionArgumentResolver { + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return OffsetScrollPosition.class.equals(parameter.getParameterType()); + } + + @Override + public OffsetScrollPosition resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) { + + String[] offsetParameter = webRequest.getParameterValues(getOffsetParameter(parameter)); + return parseParameterIntoOffsetScrollPosition(offsetParameter != null ? Arrays.asList(offsetParameter) : null); + } +} diff --git a/src/main/java/org/springframework/data/web/OffsetScrollPositionHandlerMethodArgumentResolverSupport.java b/src/main/java/org/springframework/data/web/OffsetScrollPositionHandlerMethodArgumentResolverSupport.java new file mode 100644 index 0000000000..a21cf263d7 --- /dev/null +++ b/src/main/java/org/springframework/data/web/OffsetScrollPositionHandlerMethodArgumentResolverSupport.java @@ -0,0 +1,107 @@ +/* + * Copyright 2017-2023 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.web; + +import org.springframework.core.MethodParameter; +import org.springframework.data.domain.OffsetScrollPosition; +import org.springframework.data.domain.ScrollPosition; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import java.util.List; +import java.util.Objects; + +/** + * Base class providing methods for handler method argument resolvers to create {@link OffsetScrollPosition} instances from request + * parameters. + * + * @since 3.2 + * @author Yanming Zhou + * @see OffsetScrollPositionHandlerMethodArgumentResolver + * @see ReactiveOffsetScrollPositionHandlerMethodArgumentResolver + */ +public abstract class OffsetScrollPositionHandlerMethodArgumentResolverSupport { + + private static final String DEFAULT_PARAMETER = "offset"; + + private static final String DEFAULT_QUALIFIER_DELIMITER = "_"; + + private String offsetParameter = DEFAULT_PARAMETER; + + private String qualifierDelimiter = DEFAULT_QUALIFIER_DELIMITER; + + /** + * Configure the request parameter to lookup offset information from. Defaults to {@code offset}. + * + * @param offsetParameter must not be {@literal null} or empty. + */ + public void setOffsetParameter(String offsetParameter) { + + Assert.hasText(offsetParameter, "offsetParameter must not be null nor empty"); + this.offsetParameter = offsetParameter; + } + + /** + * Configures the delimiter used to separate the qualifier from the offset parameter. Defaults to {@code _}, so a + * qualified offset property would look like {@code qualifier_offset}. + * + * @param qualifierDelimiter the qualifier delimiter to be used or {@literal null} to reset to the default. + */ + public void setQualifierDelimiter(@Nullable String qualifierDelimiter) { + this.qualifierDelimiter = qualifierDelimiter == null ? DEFAULT_QUALIFIER_DELIMITER : qualifierDelimiter; + } + + /** + * Returns the offset parameter to be looked up from the request. Potentially applies qualifiers to it. + * + * @param parameter can be {@literal null}. + * @return the offset parameter + */ + protected String getOffsetParameter(@Nullable MethodParameter parameter) { + + StringBuilder builder = new StringBuilder(); + + String value = SpringDataAnnotationUtils.getQualifier(parameter); + + if (StringUtils.hasLength(value)) { + builder.append(value); + builder.append(qualifierDelimiter); + } + + return builder.append(offsetParameter).toString(); + } + + /** + * Parses the given source into a {@link OffsetScrollPosition} instance. + * + * @param source could be {@literal null} or empty. + * @return parsed OffsetScrollPosition + */ + OffsetScrollPosition parseParameterIntoOffsetScrollPosition(@Nullable List source) { + // No parameter or Single empty parameter, e.g "offset=" + if (source == null || source.size() == 1 && !StringUtils.hasText(source.get(0))) { + return ScrollPosition.offset(); + } + try { + long offset = Long.parseLong(source.get(0)); + return ScrollPosition.offset(offset); + } catch (NumberFormatException ex) { + return ScrollPosition.offset(); + } + } + +} diff --git a/src/main/java/org/springframework/data/web/ReactiveOffsetScrollPositionHandlerMethodArgumentResolver.java b/src/main/java/org/springframework/data/web/ReactiveOffsetScrollPositionHandlerMethodArgumentResolver.java new file mode 100644 index 0000000000..690775cfb7 --- /dev/null +++ b/src/main/java/org/springframework/data/web/ReactiveOffsetScrollPositionHandlerMethodArgumentResolver.java @@ -0,0 +1,51 @@ +/* + * Copyright 2017-2023 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.web; + +import org.springframework.core.MethodParameter; +import org.springframework.data.domain.OffsetScrollPosition; +import org.springframework.lang.NonNull; +import org.springframework.web.reactive.BindingContext; +import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver; +import org.springframework.web.reactive.result.method.SyncHandlerMethodArgumentResolver; +import org.springframework.web.server.ServerWebExchange; + +import java.util.List; + +/** + * Reactive {@link HandlerMethodArgumentResolver} to create {@link OffsetScrollPosition} instances from query string parameters. + * + * @since 3.2 + * @author Yanming Zhou + */ +public class ReactiveOffsetScrollPositionHandlerMethodArgumentResolver extends OffsetScrollPositionHandlerMethodArgumentResolverSupport + implements SyncHandlerMethodArgumentResolver { + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return OffsetScrollPosition.class.equals(parameter.getParameterType()); + } + + @NonNull + @Override + public OffsetScrollPosition resolveArgumentValue(MethodParameter parameter, BindingContext bindingContext, + ServerWebExchange exchange) { + + List offsetParameter = exchange.getRequest().getQueryParams().get(getOffsetParameter(parameter)); + + return parseParameterIntoOffsetScrollPosition(offsetParameter); + } +} diff --git a/src/main/java/org/springframework/data/web/config/OffsetScrollPositionHandlerMethodArgumentResolverCustomizer.java b/src/main/java/org/springframework/data/web/config/OffsetScrollPositionHandlerMethodArgumentResolverCustomizer.java new file mode 100644 index 0000000000..50d3b17f74 --- /dev/null +++ b/src/main/java/org/springframework/data/web/config/OffsetScrollPositionHandlerMethodArgumentResolverCustomizer.java @@ -0,0 +1,35 @@ +/* + * Copyright 2017-2023 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.web.config; + +import org.springframework.data.web.OffsetScrollPositionHandlerMethodArgumentResolver; + +/** + * Callback interface that can be implemented by beans wishing to customize the + * {@link OffsetScrollPositionHandlerMethodArgumentResolver} configuration. + * + * @since 2.0 + * @author Yanming Zhou + */ +public interface OffsetScrollPositionHandlerMethodArgumentResolverCustomizer { + + /** + * Customize the given {@link OffsetScrollPositionHandlerMethodArgumentResolver}. + * + * @param offsetResolver the {@link OffsetScrollPositionHandlerMethodArgumentResolver} to customize, will never be {@literal null}. + */ + void customize(OffsetScrollPositionHandlerMethodArgumentResolver offsetResolver); +} diff --git a/src/main/java/org/springframework/data/web/config/SpringDataWebConfiguration.java b/src/main/java/org/springframework/data/web/config/SpringDataWebConfiguration.java index 98fec8de96..fba94f0a2e 100644 --- a/src/main/java/org/springframework/data/web/config/SpringDataWebConfiguration.java +++ b/src/main/java/org/springframework/data/web/config/SpringDataWebConfiguration.java @@ -28,6 +28,7 @@ import org.springframework.data.geo.format.PointFormatter; import org.springframework.data.repository.support.DomainClassConverter; import org.springframework.data.util.Lazy; +import org.springframework.data.web.OffsetScrollPositionHandlerMethodArgumentResolver; import org.springframework.data.web.PageableHandlerMethodArgumentResolver; import org.springframework.data.web.ProjectingJackson2HttpMessageConverter; import org.springframework.data.web.ProxyingHandlerMethodArgumentResolver; @@ -46,7 +47,8 @@ /** * Configuration class to register {@link PageableHandlerMethodArgumentResolver}, - * {@link SortHandlerMethodArgumentResolver} and {@link DomainClassConverter}. + * {@link SortHandlerMethodArgumentResolver}, {@link OffsetScrollPositionHandlerMethodArgumentResolver} + * and {@link DomainClassConverter}. * * @since 1.6 * @author Oliver Gierke @@ -54,6 +56,7 @@ * @author Jens Schauder * @author Mark Paluch * @author Greg Turnquist + * @author Yanming Zhou */ @Configuration(proxyBeanMethods = false) public class SpringDataWebConfiguration implements WebMvcConfigurer, BeanClassLoaderAware { @@ -66,6 +69,7 @@ public class SpringDataWebConfiguration implements WebMvcConfigurer, BeanClassLo private final Lazy pageableResolver; private final Lazy pageableResolverCustomizer; private final Lazy sortResolverCustomizer; + private final Lazy offsetResolverCustomizer; public SpringDataWebConfiguration(ApplicationContext context, @Qualifier("mvcConversionService") ObjectFactory conversionService) { @@ -83,6 +87,8 @@ public SpringDataWebConfiguration(ApplicationContext context, () -> context.getBeanProvider(PageableHandlerMethodArgumentResolverCustomizer.class).getIfAvailable()); this.sortResolverCustomizer = Lazy.of( // () -> context.getBeanProvider(SortHandlerMethodArgumentResolverCustomizer.class).getIfAvailable()); + this.offsetResolverCustomizer = Lazy.of( // + () -> context.getBeanProvider(OffsetScrollPositionHandlerMethodArgumentResolverCustomizer.class).getIfAvailable()); } @Override @@ -107,6 +113,14 @@ public SortHandlerMethodArgumentResolver sortResolver() { return sortResolver; } + @Bean + public OffsetScrollPositionHandlerMethodArgumentResolver offsetResolver() { + + OffsetScrollPositionHandlerMethodArgumentResolver offsetResolver = new OffsetScrollPositionHandlerMethodArgumentResolver(); + customizeOffsetResolver(offsetResolver); + return offsetResolver; + } + @Override public void addFormatters(FormatterRegistry registry) { @@ -165,6 +179,10 @@ protected void customizeSortResolver(SortHandlerMethodArgumentResolver sortResol sortResolverCustomizer.getOptional().ifPresent(c -> c.customize(sortResolver)); } + protected void customizeOffsetResolver(OffsetScrollPositionHandlerMethodArgumentResolver offsetResolver) { + offsetResolverCustomizer.getOptional().ifPresent(c -> c.customize(offsetResolver)); + } + private void forwardBeanClassLoader(BeanClassLoaderAware target) { if (beanClassLoader != null) { diff --git a/src/test/java/org/springframework/data/web/OffsetScrollPositionHandlerMethodArgumentResolverUnitTests.java b/src/test/java/org/springframework/data/web/OffsetScrollPositionHandlerMethodArgumentResolverUnitTests.java new file mode 100755 index 0000000000..8a95bfbee1 --- /dev/null +++ b/src/test/java/org/springframework/data/web/OffsetScrollPositionHandlerMethodArgumentResolverUnitTests.java @@ -0,0 +1,185 @@ +/* + * Copyright 2013-2023 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.web; + +import jakarta.servlet.http.HttpServletRequest; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.core.MethodParameter; +import org.springframework.data.domain.OffsetScrollPosition; +import org.springframework.data.domain.ScrollPosition; +import org.springframework.lang.Nullable; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.util.StringUtils; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.ServletWebRequest; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link OffsetScrollPositionHandlerMethodArgumentResolver}. + * + * @since 3.2 + * @author Yanming Zhou + */ +class OffsetScrollPositionHandlerMethodArgumentResolverUnitTests { + + static MethodParameter PARAMETER; + + @BeforeAll + static void setUp() throws Exception { + PARAMETER = getParameterOfMethod("supportedMethod"); + } + + @Test + void supportsSortParameter() { + + var resolver = new OffsetScrollPositionHandlerMethodArgumentResolver(); + + assertThat(resolver.supportsParameter(PARAMETER)).isTrue(); + } + + @Test + void fallbackToDefaultOffset() { + + var parameter = TestUtils.getParameterOfMethod(Controller.class, "unsupportedMethod", String.class); + var position = resolveOffset(new MockHttpServletRequest(), parameter); + assertThat(position).isEqualTo(ScrollPosition.offset()); + } + + @Test + void discoversOffsetFromRequest() { + + var reference = ScrollPosition.offset(5); + + assertSupportedAndResolvedTo(getRequestWithOffset(reference, null), PARAMETER, reference); + } + + @Test + void discoversOffsetFromRequestWithMultipleParams() { + + var request = new MockHttpServletRequest(); + request.addParameter("offset", "5"); + request.addParameter("offset", "6"); + + assertThat(resolveOffset(request, PARAMETER)).isEqualTo(ScrollPosition.offset(5)); + } + + @Test + void discoversQualifiedOffsetFromRequest() { + + var parameter = getParameterOfMethod("qualifiedOffset"); + var reference = ScrollPosition.offset(5); + + assertSupportedAndResolvedTo(getRequestWithOffset(reference, "qual"), parameter, reference); + } + + @Test + void returnsDefaultForOffsetParamSetToNothing() { + + var request = new MockHttpServletRequest(); + request.addParameter("offset", (String) null); + + assertThat(resolveOffset(request, PARAMETER)).isEqualTo(ScrollPosition.offset()); + } + + @Test + void returnsDefaultForEmptyOffsetParam() { + + var request = new MockHttpServletRequest(); + request.addParameter("offset", ""); + + assertThat(resolveOffset(request, PARAMETER)).isEqualTo(ScrollPosition.offset()); + } + + @Test + void returnsDefaultForOffsetParamIsInvalidProperty() { + + var request = new MockHttpServletRequest(); + request.addParameter("offset", "invalid_number"); + + assertThat(resolveOffset(request, PARAMETER)).isEqualTo(ScrollPosition.offset()); + } + + @Test + void emptyQualifierIsUsedInParameterLookup() { + + var parameter = getParameterOfMethod("emptyQualifier"); + var reference = ScrollPosition.offset(5); + + assertSupportedAndResolvedTo(getRequestWithOffset(reference, ""), parameter, reference); + } + + @Test + void mergedQualifierIsUsedInParameterLookup() { + + var parameter = getParameterOfMethod("mergedQualifier"); + var reference = ScrollPosition.offset(5); + + assertSupportedAndResolvedTo(getRequestWithOffset(reference, "merged"), parameter, reference); + } + + private static OffsetScrollPosition resolveOffset(HttpServletRequest request, MethodParameter parameter) { + + var resolver = new OffsetScrollPositionHandlerMethodArgumentResolver(); + return resolver.resolveArgument(parameter, null, new ServletWebRequest(request), null); + } + + private static void assertSupportedAndResolvedTo(NativeWebRequest request, MethodParameter parameter, OffsetScrollPosition position) { + + var resolver = new OffsetScrollPositionHandlerMethodArgumentResolver(); + assertThat(resolver.supportsParameter(parameter)).isTrue(); + + try { + assertThat(resolver.resolveArgument(parameter, null, request, null)).isEqualTo(position); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + + private static MethodParameter getParameterOfMethod(String name) { + return TestUtils.getParameterOfMethod(Controller.class, name, OffsetScrollPosition.class); + } + + private static NativeWebRequest getRequestWithOffset(@Nullable OffsetScrollPosition position, @Nullable String qualifier) { + + var request = new MockHttpServletRequest(); + + if (position == null) { + return new ServletWebRequest(request); + } + + String parameterName = StringUtils.hasLength(qualifier) ? qualifier + "_offset" : "offset"; + request.addParameter(parameterName, String.valueOf(position.getOffset())); + + return new ServletWebRequest(request); + } + + interface Controller { + + void supportedMethod(OffsetScrollPosition offset); + + void unsupportedMethod(String string); + + void qualifiedOffset(@Qualifier("qual") OffsetScrollPosition offset); + + void emptyQualifier(@Qualifier OffsetScrollPosition offset); + + void mergedQualifier(@TestQualifier OffsetScrollPosition offset); + } +} diff --git a/src/test/java/org/springframework/data/web/ReactiveOffsetScrollPositionHandlerMethodArgumentResolverUnitTests.java b/src/test/java/org/springframework/data/web/ReactiveOffsetScrollPositionHandlerMethodArgumentResolverUnitTests.java new file mode 100755 index 0000000000..094d9aa70d --- /dev/null +++ b/src/test/java/org/springframework/data/web/ReactiveOffsetScrollPositionHandlerMethodArgumentResolverUnitTests.java @@ -0,0 +1,176 @@ +/* + * Copyright 2017-2023 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.web; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.core.MethodParameter; +import org.springframework.data.domain.OffsetScrollPosition; +import org.springframework.data.domain.ScrollPosition; +import org.springframework.lang.Nullable; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; +import org.springframework.util.StringUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link ReactiveOffsetScrollPositionHandlerMethodArgumentResolver}. + * + * @since 3.2 + * @author Yanming Zhou + */ +class ReactiveOffsetScrollPositionHandlerMethodArgumentResolverUnitTests { + + static MethodParameter PARAMETER; + + @BeforeAll + static void setUp() throws Exception { + PARAMETER = getParameterOfMethod("supportedMethod"); + } + + @Test + void supportsSortParameter() { + + var resolver = new ReactiveOffsetScrollPositionHandlerMethodArgumentResolver(); + + assertThat(resolver.supportsParameter(PARAMETER)).isTrue(); + } + + @Test + void fallbackToDefaultOffset() { + + var parameter = TestUtils.getParameterOfMethod(Controller.class, "unsupportedMethod", String.class); + var request = MockServerHttpRequest.get("/foo").build(); + var position = resolveOffset(request, parameter); + assertThat(position).isEqualTo(ScrollPosition.offset()); + } + + @Test + void discoversOffsetFromRequest() { + + var reference = ScrollPosition.offset(5); + + assertSupportedAndResolvedTo(getRequestWithOffset(reference, null), PARAMETER, reference); + } + + @Test + void discoversOffsetFromRequestWithMultipleParams() { + + var request = MockServerHttpRequest.get("/foo?offset=5&offset=6").build(); + + assertThat(resolveOffset(request, PARAMETER)).isEqualTo(ScrollPosition.offset(5)); + } + + @Test + void discoversQualifiedOffsetFromRequest() { + + var parameter = getParameterOfMethod("qualifiedOffset"); + var reference = ScrollPosition.offset(5); + + assertSupportedAndResolvedTo(getRequestWithOffset(reference, "qual"), parameter, reference); + } + + @Test + void returnsDefaultForOffsetParamSetToNothing() { + + var request = MockServerHttpRequest.get("/foo").build(); + + assertThat(resolveOffset(request, PARAMETER)).isEqualTo(ScrollPosition.offset()); + } + + @Test + void returnsDefaultForEmptyOffsetParam() { + + var request = MockServerHttpRequest.get("/foo?offset=").build(); + + assertThat(resolveOffset(request, PARAMETER)).isEqualTo(ScrollPosition.offset()); + } + + @Test + void returnsDefaultForOffsetParamIsInvalidProperty() { + + var request = MockServerHttpRequest.get("/foo?offset=invalid_number").build(); + + assertThat(resolveOffset(request, PARAMETER)).isEqualTo(ScrollPosition.offset()); + } + + @Test + void emptyQualifierIsUsedInParameterLookup() { + + var parameter = getParameterOfMethod("emptyQualifier"); + var reference = ScrollPosition.offset(5); + + assertSupportedAndResolvedTo(getRequestWithOffset(reference, ""), parameter, reference); + } + + @Test + void mergedQualifierIsUsedInParameterLookup() { + + var parameter = getParameterOfMethod("mergedQualifier"); + var reference = ScrollPosition.offset(5); + + assertSupportedAndResolvedTo(getRequestWithOffset(reference, "merged"), parameter, reference); + } + + private static OffsetScrollPosition resolveOffset(MockServerHttpRequest request, MethodParameter parameter) { + + var resolver = new ReactiveOffsetScrollPositionHandlerMethodArgumentResolver(); + return resolver.resolveArgumentValue(parameter, null, MockServerWebExchange.from(request)); + } + + private static void assertSupportedAndResolvedTo(MockServerHttpRequest request, MethodParameter parameter, OffsetScrollPosition position) { + + var resolver = new ReactiveOffsetScrollPositionHandlerMethodArgumentResolver(); + assertThat(resolver.supportsParameter(parameter)).isTrue(); + + try { + var resolved = resolveOffset(request, parameter); + assertThat(resolved).isEqualTo(position); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + + private static MethodParameter getParameterOfMethod(String name) { + return TestUtils.getParameterOfMethod(Controller.class, name, OffsetScrollPosition.class); + } + + private static MockServerHttpRequest getRequestWithOffset(@Nullable OffsetScrollPosition position, @Nullable String qualifier) { + + if (position == null) { + return TestUtils.getWebfluxRequest(); + } + + String parameterName = StringUtils.hasLength(qualifier) ? qualifier + "_offset" : "offset"; + return MockServerHttpRequest.get(String.format("foo?%s=%s", parameterName, String.valueOf(position.getOffset()))).build(); + } + + interface Controller { + + void supportedMethod(OffsetScrollPosition offset); + + void unsupportedMethod(String string); + + void qualifiedOffset(@Qualifier("qual") OffsetScrollPosition offset); + + void emptyQualifier(@Qualifier OffsetScrollPosition offset); + + void mergedQualifier(@TestQualifier OffsetScrollPosition offset); + } +} diff --git a/src/test/java/org/springframework/data/web/config/EnableSpringDataWebSupportIntegrationTests.java b/src/test/java/org/springframework/data/web/config/EnableSpringDataWebSupportIntegrationTests.java index 1884b40d95..496bb02973 100755 --- a/src/test/java/org/springframework/data/web/config/EnableSpringDataWebSupportIntegrationTests.java +++ b/src/test/java/org/springframework/data/web/config/EnableSpringDataWebSupportIntegrationTests.java @@ -32,6 +32,7 @@ import org.springframework.data.querydsl.EntityPathResolver; import org.springframework.data.querydsl.SimpleEntityPathResolver; import org.springframework.data.querydsl.binding.QuerydslBindingsFactory; +import org.springframework.data.web.OffsetScrollPositionHandlerMethodArgumentResolver; import org.springframework.data.web.PageableHandlerMethodArgumentResolver; import org.springframework.data.web.PagedResourcesAssemblerArgumentResolver; import org.springframework.data.web.ProxyingHandlerMethodArgumentResolver; @@ -52,6 +53,7 @@ * @author Oliver Gierke * @author Jens Schauder * @author Vedran Pavic + * @author Yanming Zhou */ class EnableSpringDataWebSupportIntegrationTests { @@ -88,6 +90,17 @@ SortHandlerMethodArgumentResolverCustomizer testSortResolverCustomizer() { } } + @Configuration + @EnableWebMvc + @EnableSpringDataWebSupport + static class OffsetResolverCustomizerConfig extends SampleConfig { + + @Bean + OffsetScrollPositionHandlerMethodArgumentResolverCustomizer testOffsetResolverCustomizer() { + return offsetResolver -> offsetResolver.setOffsetParameter("foo"); + } + } + @Configuration @EnableSpringDataWebSupport static class CustomEntityPathResolver { @@ -217,6 +230,17 @@ void picksUpSortResolverCustomizer() { assertThat((String) ReflectionTestUtils.getField(resolver, "sortParameter")).isEqualTo("foo"); } + @Test + void picksUpOffsetResolverCustomizer() { + + ApplicationContext context = WebTestUtils.createApplicationContext(OffsetResolverCustomizerConfig.class); + var names = Arrays.asList(context.getBeanDefinitionNames()); + var resolver = context.getBean("offsetResolver", OffsetScrollPositionHandlerMethodArgumentResolver.class); + + assertThat(names).contains("testOffsetResolverCustomizer"); + assertThat((String) ReflectionTestUtils.getField(resolver, "offsetParameter")).isEqualTo("foo"); + } + @Test // DATACMNS-1237 void configuresProxyingHandlerMethodArgumentResolver() {