diff --git a/src/main/java/org/springframework/data/web/MethodParameterAwarePagedResourcesAssembler.java b/src/main/java/org/springframework/data/web/MethodParameterAwarePagedResourcesAssembler.java deleted file mode 100644 index 05a55bd2b..000000000 --- a/src/main/java/org/springframework/data/web/MethodParameterAwarePagedResourcesAssembler.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2014-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.lang.NonNull; -import org.springframework.lang.Nullable; -import org.springframework.util.Assert; -import org.springframework.web.util.UriComponents; - -/** - * Custom {@link PagedResourcesAssembler} that is aware of the {@link MethodParameter} it shall create links for. - * - * @author Oliver Gierke - * @since 1.7 - */ -class MethodParameterAwarePagedResourcesAssembler extends PagedResourcesAssembler { - - private final MethodParameter parameter; - - /** - * Creates a new {@link MethodParameterAwarePagedResourcesAssembler} using the given {@link MethodParameter}, - * {@link HateoasPageableHandlerMethodArgumentResolver} and base URI. - * - * @param parameter must not be {@literal null}. - * @param resolver can be {@literal null}. - * @param baseUri can be {@literal null}. - */ - public MethodParameterAwarePagedResourcesAssembler(MethodParameter parameter, - @Nullable HateoasPageableHandlerMethodArgumentResolver resolver, @Nullable UriComponents baseUri) { - - super(resolver, baseUri); - - Assert.notNull(parameter, "Method parameter must not be null"); - this.parameter = parameter; - } - - @NonNull - @Override - protected MethodParameter getMethodParameter() { - return parameter; - } -} diff --git a/src/main/java/org/springframework/data/web/MethodParameterAwareSlicedResourcesAssembler.java b/src/main/java/org/springframework/data/web/MethodParameterAwareSlicedResourcesAssembler.java deleted file mode 100644 index f98f51998..000000000 --- a/src/main/java/org/springframework/data/web/MethodParameterAwareSlicedResourcesAssembler.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2022 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.lang.NonNull; -import org.springframework.lang.Nullable; -import org.springframework.util.Assert; -import org.springframework.web.util.UriComponents; - -/** - * Custom {@link SlicedResourcesAssembler} that is aware of the {@link MethodParameter} it shall create links for. - * - * @author Michael Schout - */ -public class MethodParameterAwareSlicedResourcesAssembler extends SlicedResourcesAssembler { - private final MethodParameter parameter; - - /** - * Creates a new {@link MethodParameterAwareSlicedResourcesAssembler} using the given - * {@link MethodParameter}, {@link HateoasPageableHandlerMethodArgumentResolver} and base - * URI. - * - * @param parameter must not be {@literal null}. - * @param resolver can be {@literal null}. - * @param baseUri can be {@literal null}. - */ - public MethodParameterAwareSlicedResourcesAssembler(MethodParameter parameter, - @Nullable HateoasPageableHandlerMethodArgumentResolver resolver, @Nullable UriComponents baseUri) { - - super(resolver, baseUri); - - Assert.notNull(parameter, "Method parameter must not be null"); - this.parameter = parameter; - } - - @NonNull - @Override - protected MethodParameter getMethodParameter() { - return parameter; - } -} diff --git a/src/main/java/org/springframework/data/web/PageableMethodParameterUtils.java b/src/main/java/org/springframework/data/web/PageableMethodParameterUtils.java new file mode 100644 index 000000000..43093bddc --- /dev/null +++ b/src/main/java/org/springframework/data/web/PageableMethodParameterUtils.java @@ -0,0 +1,111 @@ +/* + * Copyright 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 java.lang.reflect.Method; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.core.MethodParameter; +import org.springframework.core.log.LogMessage; +import org.springframework.data.domain.Pageable; +import org.springframework.hateoas.server.core.MethodParameters; +import org.springframework.lang.Nullable; + +/** + * Utility methods to obtain a {@link MethodParameter} of type {@link Pageable} with the same {@link Qualifier}. + * + * @author Oliver Drotbohm + * @since 3.1 + * @soundtrack The Intersphere - Wanderer (https://www.youtube.com/watch?v=Sp_VyFBbDPA) + */ +class PageableMethodParameterUtils { + + private static final Log logger = LogFactory.getLog(PageableMethodParameterUtils.class); + + private static final String SUPERFLOUS_QUALIFIER = "Found qualified %s parameter, but a unique unqualified %s parameter; Using that one, but you might want to check your controller method configuration"; + private static final String PARAMETER_AMBIGUITY = "Discovered multiple parameters of type Pageable but no qualifier annotations to disambiguate"; + + /** + * Returns finds the {@link MethodParameter} for a {@link Pageable} instance matching the given + * {@link MethodParameter} requesting a {@link PagedResourcesAssembler}. + * + * @param parameter must not be {@literal null}. + * @return can be {@literal null}. + */ + @Nullable + static MethodParameter findMatchingPageableParameter(MethodParameter parameter) { + + Method method = parameter.getMethod(); + + if (method == null) { + throw new IllegalArgumentException(String.format("Could not obtain method from parameter %s", parameter)); + } + + MethodParameters parameters = MethodParameters.of(method); + List pageableParameters = parameters.getParametersOfType(Pageable.class); + Qualifier assemblerQualifier = parameter.getParameterAnnotation(Qualifier.class); + + if (pageableParameters.isEmpty()) { + return null; + } + + if (pageableParameters.size() == 1) { + + MethodParameter pageableParameter = pageableParameters.get(0); + MethodParameter matchingParameter = returnIfQualifiersMatch(pageableParameter, assemblerQualifier); + + if (matchingParameter == null) { + logger.info(LogMessage.format(SUPERFLOUS_QUALIFIER, PagedResourcesAssembler.class.getSimpleName(), + Pageable.class.getName())); + } + + return pageableParameter; + } + + if (assemblerQualifier == null) { + throw new IllegalStateException(PARAMETER_AMBIGUITY); + } + + for (MethodParameter pageableParameter : pageableParameters) { + + MethodParameter matchingParameter = returnIfQualifiersMatch(pageableParameter, assemblerQualifier); + + if (matchingParameter != null) { + return matchingParameter; + } + } + + throw new IllegalStateException(PARAMETER_AMBIGUITY); + } + + @Nullable + private static MethodParameter returnIfQualifiersMatch(MethodParameter pageableParameter, + @Nullable Qualifier assemblerQualifier) { + + if (assemblerQualifier == null) { + return pageableParameter; + } + + Qualifier pageableParameterQualifier = pageableParameter.getParameterAnnotation(Qualifier.class); + + return pageableParameterQualifier != null + ? pageableParameterQualifier.value().equals(assemblerQualifier.value()) ? pageableParameter : null + : null; + } +} diff --git a/src/main/java/org/springframework/data/web/PagedResourcesAssembler.java b/src/main/java/org/springframework/data/web/PagedResourcesAssembler.java index 40f2fd0f6..84da129ef 100644 --- a/src/main/java/org/springframework/data/web/PagedResourcesAssembler.java +++ b/src/main/java/org/springframework/data/web/PagedResourcesAssembler.java @@ -58,6 +58,7 @@ public class PagedResourcesAssembler implements RepresentationModelAssembler< private final EmbeddedWrappers wrappers = new EmbeddedWrappers(false); private boolean forceFirstAndLastRels = false; + private @Nullable MethodParameter parameter; /** * Creates a new {@link PagedResourcesAssembler} using the given {@link PageableHandlerMethodArgumentResolver} and @@ -69,9 +70,15 @@ public class PagedResourcesAssembler implements RepresentationModelAssembler< */ public PagedResourcesAssembler(@Nullable HateoasPageableHandlerMethodArgumentResolver resolver, @Nullable UriComponents baseUri) { + this(resolver, Optional.ofNullable(baseUri), null); + } + + private PagedResourcesAssembler(@Nullable HateoasPageableHandlerMethodArgumentResolver resolver, + Optional baseUri, @Nullable MethodParameter parameter) { this.pageableResolver = resolver == null ? new HateoasPageableHandlerMethodArgumentResolver() : resolver; - this.baseUri = Optional.ofNullable(baseUri); + this.baseUri = baseUri; + this.parameter = parameter; } /** @@ -87,6 +94,17 @@ public class PagedResourcesAssembler implements RepresentationModelAssembler< this.forceFirstAndLastRels = forceFirstAndLastRels; } + /** + * Creates a new {@link PagedResourcesAssembler} with the given reference {@link MethodParameter}. + * + * @param parameter can be {@literal null}. + * @return will never be {@literal null}. + * @since 3.1 + */ + public PagedResourcesAssembler withParameter(@Nullable MethodParameter parameter) { + return new PagedResourcesAssembler<>(pageableResolver, baseUri, parameter); + } + @Override public PagedModel> toModel(Page entity) { return toModel(entity, EntityModel::of); @@ -267,7 +285,7 @@ public class PagedResourcesAssembler implements RepresentationModelAssembler< private Link createLink(UriTemplate base, Pageable pageable, LinkRelation relation) { UriComponentsBuilder builder = fromUri(base.expand()); - pageableResolver.enhance(builder, getMethodParameter(), pageable); + pageableResolver.enhance(builder, parameter, pageable); return Link.of(UriTemplate.of(builder.build().toString()), relation); } @@ -278,8 +296,10 @@ public class PagedResourcesAssembler implements RepresentationModelAssembler< * * @return * @since 1.7 + * @deprecated since 3.1, rather set up the instance with {@link #withParameter(MethodParameter)}. */ @Nullable + @Deprecated(since = "3.1", forRemoval = true) protected MethodParameter getMethodParameter() { return null; } diff --git a/src/main/java/org/springframework/data/web/PagedResourcesAssemblerArgumentResolver.java b/src/main/java/org/springframework/data/web/PagedResourcesAssemblerArgumentResolver.java index f2d0dc8b4..2a2fb98fd 100644 --- a/src/main/java/org/springframework/data/web/PagedResourcesAssemblerArgumentResolver.java +++ b/src/main/java/org/springframework/data/web/PagedResourcesAssemblerArgumentResolver.java @@ -15,17 +15,7 @@ */ package org.springframework.data.web; -import java.lang.reflect.Method; -import java.util.List; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.core.MethodParameter; -import org.springframework.core.log.LogMessage; -import org.springframework.data.domain.Pageable; -import org.springframework.hateoas.server.core.MethodParameters; import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.springframework.web.bind.support.WebDataBinderFactory; @@ -45,11 +35,6 @@ import org.springframework.web.method.support.ModelAndViewContainer; */ public class PagedResourcesAssemblerArgumentResolver implements HandlerMethodArgumentResolver { - private static final Log logger = LogFactory.getLog(PagedResourcesAssemblerArgumentResolver.class); - - private static final String SUPERFLOUS_QUALIFIER = "Found qualified %s parameter, but a unique unqualified %s parameter; Using that one, but you might want to check your controller method configuration"; - private static final String PARAMETER_AMBIGUITY = "Discovered multiple parameters of type Pageable but no qualifier annotations to disambiguate"; - private final HateoasPageableHandlerMethodArgumentResolver resolver; /** @@ -72,82 +57,7 @@ public class PagedResourcesAssemblerArgumentResolver implements HandlerMethodArg public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) { - MethodParameter pageableParameter = findMatchingPageableParameter(parameter); - - if (pageableParameter != null) { - return new MethodParameterAwarePagedResourcesAssembler<>(pageableParameter, resolver, null); - } else { - return new PagedResourcesAssembler<>(resolver, null); - } - } - - /** - * Returns finds the {@link MethodParameter} for a {@link Pageable} instance matching the given - * {@link MethodParameter} requesting a {@link PagedResourcesAssembler}. - * - * @param parameter must not be {@literal null}. - * @return - */ - @Nullable - private static MethodParameter findMatchingPageableParameter(MethodParameter parameter) { - - Method method = parameter.getMethod(); - - if (method == null) { - throw new IllegalArgumentException(String.format("Could not obtain method from parameter %s", parameter)); - } - - MethodParameters parameters = MethodParameters.of(method); - List pageableParameters = parameters.getParametersOfType(Pageable.class); - Qualifier assemblerQualifier = parameter.getParameterAnnotation(Qualifier.class); - - if (pageableParameters.isEmpty()) { - return null; - } - - if (pageableParameters.size() == 1) { - - MethodParameter pageableParameter = pageableParameters.get(0); - MethodParameter matchingParameter = returnIfQualifiersMatch(pageableParameter, assemblerQualifier); - - if (matchingParameter == null) { - logger.info(LogMessage.format(SUPERFLOUS_QUALIFIER, PagedResourcesAssembler.class.getSimpleName(), - Pageable.class.getName())); - } - - return pageableParameter; - } - - if (assemblerQualifier == null) { - throw new IllegalStateException(PARAMETER_AMBIGUITY); - } - - for (MethodParameter pageableParameter : pageableParameters) { - - MethodParameter matchingParameter = returnIfQualifiersMatch(pageableParameter, assemblerQualifier); - - if (matchingParameter != null) { - return matchingParameter; - } - } - - throw new IllegalStateException(PARAMETER_AMBIGUITY); - } - - @Nullable - private static MethodParameter returnIfQualifiersMatch(MethodParameter pageableParameter, - @Nullable Qualifier assemblerQualifier) { - - if (assemblerQualifier == null) { - return pageableParameter; - } - - Qualifier pageableParameterQualifier = pageableParameter.getParameterAnnotation(Qualifier.class); - - if (pageableParameterQualifier == null) { - return null; - } - - return pageableParameterQualifier.value().equals(assemblerQualifier.value()) ? pageableParameter : null; + return new PagedResourcesAssembler<>(resolver, null) + .withParameter(PageableMethodParameterUtils.findMatchingPageableParameter(parameter)); } } diff --git a/src/main/java/org/springframework/data/web/SlicedResourcesAssembler.java b/src/main/java/org/springframework/data/web/SlicedResourcesAssembler.java index 63290c8ff..79a883f5b 100644 --- a/src/main/java/org/springframework/data/web/SlicedResourcesAssembler.java +++ b/src/main/java/org/springframework/data/web/SlicedResourcesAssembler.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 the original author or authors. + * Copyright 2022-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. @@ -15,7 +15,7 @@ */ package org.springframework.data.web; -import static org.springframework.web.util.UriComponentsBuilder.fromUri; +import static org.springframework.web.util.UriComponentsBuilder.*; import java.util.ArrayList; import java.util.Collections; @@ -26,8 +26,14 @@ import org.springframework.core.MethodParameter; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; -import org.springframework.hateoas.*; +import org.springframework.hateoas.EntityModel; +import org.springframework.hateoas.IanaLinkRelations; +import org.springframework.hateoas.Link; +import org.springframework.hateoas.LinkRelation; +import org.springframework.hateoas.RepresentationModel; +import org.springframework.hateoas.SlicedModel; import org.springframework.hateoas.SlicedModel.SliceMetadata; +import org.springframework.hateoas.UriTemplate; import org.springframework.hateoas.server.RepresentationModelAssembler; import org.springframework.hateoas.server.core.EmbeddedWrapper; import org.springframework.hateoas.server.core.EmbeddedWrappers; @@ -38,62 +44,74 @@ import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; /** - * {@link RepresentationModelAssembler} to easily convert {@link Slice} instances into - * {@link SlicedModel}. + * {@link RepresentationModelAssembler} to easily convert {@link Slice} instances into {@link SlicedModel}. * * @author Michael Schout + * @author Oliver Drotbohm + * @since 3.1 */ public class SlicedResourcesAssembler implements RepresentationModelAssembler, SlicedModel>> { private final HateoasPageableHandlerMethodArgumentResolver pageableResolver; - private final Optional baseUri; private final EmbeddedWrappers wrappers = new EmbeddedWrappers(false); private boolean forceFirstRel = false; + private @Nullable MethodParameter parameter; /** - * Creates a new {@link SlicedResourcesAssembler} using the given - * {@link PageableHandlerMethodArgumentResolver} and base URI. If the former is - * {@literal null}, a default one will be created. If the latter is {@literal null}, calls - * to {@link #toModel(Slice)} will use the current request's URI to build the relevant - * previous and next links. + * Creates a new {@link SlicedResourcesAssembler} using the given {@link PageableHandlerMethodArgumentResolver} and + * base URI. If the former is {@literal null}, a default one will be created. If the latter is {@literal null}, calls + * to {@link #toModel(Slice)} will use the current request's URI to build the relevant previous and next links. * * @param resolver can be {@literal null}. * @param baseUri can be {@literal null}. */ public SlicedResourcesAssembler(@Nullable HateoasPageableHandlerMethodArgumentResolver resolver, @Nullable UriComponents baseUri) { - this.pageableResolver = resolver == null ? new HateoasPageableHandlerMethodArgumentResolver() : resolver; - this.baseUri = Optional.ofNullable(baseUri); + this(resolver, Optional.ofNullable(baseUri), null); } - private static String currentRequest() { - return ServletUriComponentsBuilder.fromCurrentRequest().build().toString(); + private SlicedResourcesAssembler(@Nullable HateoasPageableHandlerMethodArgumentResolver resolver, + @Nullable Optional baseUri, @Nullable MethodParameter parameter) { + + this.pageableResolver = resolver == null ? new HateoasPageableHandlerMethodArgumentResolver() : resolver; + this.baseUri = baseUri; + this.parameter = parameter; } /** - * Configures whether to always add {@code first} links to the {@link SlicedModel} * - * created. Defaults to {@literal false} which means that {@code first} links onlys appear - * in conjunction with {@code prev} and {@code next} links. + * Configures whether to always add {@code first} links to the {@link SlicedModel} * created. Defaults to + * {@literal false} which means that {@code first} links onlys appear in conjunction with {@code prev} and + * {@code next} links. * - * @param forceFirstRel whether to always add {@code first} links to the - * {@link SlicedModel} created. + * @param forceFirstRel whether to always add {@code first} links to the {@link SlicedModel} created. */ public void setForceFirstRel(boolean forceFirstRel) { this.forceFirstRel = forceFirstRel; } + /** + * Creates a new {@link SlicedResourcesAssembler} with the given reference {@link MethodParameter}. + * + * @param parameter can be {@literal null}. + * @return will never be {@literal null}. + * @since 3.1 + */ + public SlicedResourcesAssembler withParameter(@Nullable MethodParameter parameter) { + return new SlicedResourcesAssembler<>(pageableResolver, baseUri, parameter); + } + @Override public SlicedModel> toModel(Slice entity) { return toModel(entity, EntityModel::of); } /** - * Creates a new {@link SlicedModel} by converting the given {@link Slice} into a - * {@link SliceMetadata} instance and wrapping the contained elements into * - * {@link SlicedModel} instances. Will add pagination links based on the given self link. + * Creates a new {@link SlicedModel} by converting the given {@link Slice} into a {@link SliceMetadata} instance and + * wrapping the contained elements into * {@link SlicedModel} instances. Will add pagination links based on the given + * self link. * * @param slice must not be {@literal null}. * @param selfLink must not be {@literal null}. @@ -104,9 +122,8 @@ public class SlicedResourcesAssembler } /** - * Creates a new {@link SlicedModel} by converting the given {@link Slice} into a - * {@link SliceMetadata} instance and using the given {@link SlicedModel} to turn elements - * of the {@link Slice} into resources. + * Creates a new {@link SlicedModel} by converting the given {@link Slice} into a {@link SliceMetadata} instance and + * using the given {@link SlicedModel} to turn elements of the {@link Slice} into resources. * * @param slice must not be {@literal null}. * @param assembler must not be {@literal null}. @@ -118,10 +135,9 @@ public class SlicedResourcesAssembler } /** - * Creates a new {@link SlicedModel} by converting the given {@link Slice} into a - * {@link SliceMetadata} instance and using the given {@link SlicedModel} to turn elements - * of the {@link Slice} into resources. Will add pagination links based on the given the - * self link. + * Creates a new {@link SlicedModel} by converting the given {@link Slice} into a {@link SliceMetadata} instance and + * using the given {@link SlicedModel} to turn elements of the {@link Slice} into resources. Will add pagination links + * based on the given the self link. * * @param slice must not be {@literal null}. * @param assembler must not be {@literal null}. @@ -134,8 +150,7 @@ public class SlicedResourcesAssembler } /** - * Creates a {@link SlicedModel} with an empty collection {@link EmbeddedWrapper} for the - * given domain type. + * Creates a {@link SlicedModel} with an empty collection {@link EmbeddedWrapper} for the given domain type. * * @param slice must not be {@literal null}, content must be empty. * @param type must not be {@literal null}. @@ -146,8 +161,7 @@ public class SlicedResourcesAssembler } /** - * Creates a {@link SlicedModel} with an empty collection {@link EmbeddedWrapper} for the - * given domain type. + * Creates a {@link SlicedModel} with an empty collection {@link EmbeddedWrapper} for the given domain type. * * @param slice must not be {@literal null}, content must be empty. * @param type must not be {@literal null}. @@ -159,6 +173,7 @@ public class SlicedResourcesAssembler } public SlicedModel toEmptyModel(Slice slice, Class type, Optional link) { + Assert.notNull(slice, "Slice must not be null"); Assert.isTrue(!slice.hasContent(), "Slice must not have any content"); Assert.notNull(type, "Type must not be null"); @@ -175,8 +190,7 @@ public class SlicedResourcesAssembler /** * Creates the {@link SlicedModel} to be equipped with pagination links downstream. * - * @param resources the original slices's elements mapped into {@link RepresentationModel} - * instances. + * @param resources the original slices's elements mapped into {@link RepresentationModel} instances. * @param metadata the calculated {@link SliceMetadata}, must not be {@literal null}. * @param slice the original page handed to the assembler, must not be {@literal null}. * @return must not be {@literal null}. @@ -233,8 +247,8 @@ public class SlicedResourcesAssembler } /** - * Returns a default URI string either from the one configured on then assembler or by - * looking it up from the current request. + * Returns a default URI string either from the one configured on then assembler or by looking it up from the current + * request. * * @return */ @@ -243,9 +257,8 @@ public class SlicedResourcesAssembler } /** - * Creates a {@link Link} with the given {@link LinkRelation} that will be based on the - * given {@link UriTemplate} but enriched with the values of the given {@link Pageable} - * (if not {@literal null}). + * Creates a {@link Link} with the given {@link LinkRelation} that will be based on the given {@link UriTemplate} but + * enriched with the values of the given {@link Pageable} (if not {@literal null}). * * @param base must not be {@literal null}. * @param pageable can be {@literal null} @@ -254,23 +267,11 @@ public class SlicedResourcesAssembler */ private Link createLink(UriTemplate base, Pageable pageable, LinkRelation relation) { UriComponentsBuilder builder = fromUri(base.expand()); - pageableResolver.enhance(builder, getMethodParameter(), pageable); + pageableResolver.enhance(builder, parameter, pageable); return Link.of(UriTemplate.of(builder.build().toString()), relation); } - /** - * Return the {@link MethodParameter} to be used to potentially qualify the paging and - * sorting request parameters to. Default implementations returns {@literal null}, which - * means the parameters will not be qualified. - * - * @return - */ - @Nullable - protected MethodParameter getMethodParameter() { - return null; - } - /** * Creates a new {@link SliceMetadata} instance from the given {@link Slice}. * @@ -288,4 +289,8 @@ public class SlicedResourcesAssembler private String baseUriOrCurrentRequest() { return baseUri.map(Object::toString).orElseGet(SlicedResourcesAssembler::currentRequest); } -} \ No newline at end of file + + private static String currentRequest() { + return ServletUriComponentsBuilder.fromCurrentRequest().build().toString(); + } +} diff --git a/src/main/java/org/springframework/data/web/SlicedResourcesAssemblerArgumentResolver.java b/src/main/java/org/springframework/data/web/SlicedResourcesAssemblerArgumentResolver.java index a38f5c28e..eb661dd85 100644 --- a/src/main/java/org/springframework/data/web/SlicedResourcesAssemblerArgumentResolver.java +++ b/src/main/java/org/springframework/data/web/SlicedResourcesAssemblerArgumentResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 the original author or authors. + * Copyright 2022-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. @@ -15,16 +15,7 @@ */ package org.springframework.data.web; -import java.lang.reflect.Method; -import java.util.List; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.core.MethodParameter; -import org.springframework.core.log.LogMessage; -import org.springframework.data.domain.Pageable; -import org.springframework.hateoas.server.core.MethodParameters; import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.springframework.web.bind.support.WebDataBinderFactory; @@ -37,12 +28,10 @@ import org.springframework.web.method.support.ModelAndViewContainer; * controller methods. * * @author Michael Schout + * @author Oliver Drotbohm + * @since 3.1 */ public class SlicedResourcesAssemblerArgumentResolver implements HandlerMethodArgumentResolver { - private static final Log logger = LogFactory.getLog(SlicedResourcesAssemblerArgumentResolver.class); - - private static final String SUPERFLOUS_QUALIFIER = "Found qualified %s parameter, but a unique unqualified %s parameter; Using that one, but you might want to check your controller method configuration"; - private static final String PARAMETER_AMBIGUITY = "Discovered multiple parameters of type Pageable but no qualifier annotations to disambiguate"; private final HateoasPageableHandlerMethodArgumentResolver resolver; @@ -56,73 +45,6 @@ public class SlicedResourcesAssemblerArgumentResolver implements HandlerMethodAr this.resolver = resolver; } - /** - * Returns finds the {@link MethodParameter} for a {@link Pageable} instance matching the - * given {@link MethodParameter} requesting a {@link SlicedResourcesAssembler}. - * - * @param parameter must not be {@literal null}. - * @return - */ - @Nullable - private static MethodParameter findMatchingPageableParameter(MethodParameter parameter) { - Method method = parameter.getMethod(); - - if (method == null) { - throw new IllegalArgumentException(String.format("Could not obtain method from parameter %s", parameter)); - } - - MethodParameters parameters = MethodParameters.of(method); - List pageableParameters = parameters.getParametersOfType(Pageable.class); - Qualifier assemblerQualifier = parameter.getParameterAnnotation(Qualifier.class); - - if (pageableParameters.isEmpty()) { - return null; - } - - if (pageableParameters.size() == 1) { - MethodParameter pageableParameter = pageableParameters.get(0); - MethodParameter matchingParameter = returnIfQualifiersMatch(pageableParameter, assemblerQualifier); - - if (matchingParameter == null) { - logger.info(LogMessage.format(SUPERFLOUS_QUALIFIER, SlicedResourcesAssembler.class.getSimpleName(), - Pageable.class.getName())); - } - - return pageableParameter; - } - - if (assemblerQualifier == null) { - throw new IllegalStateException(PARAMETER_AMBIGUITY); - } - - for (MethodParameter pageableParameter : pageableParameters) { - MethodParameter matchingParameter = returnIfQualifiersMatch(pageableParameter, assemblerQualifier); - - if (matchingParameter != null) { - return matchingParameter; - } - } - - throw new IllegalStateException(PARAMETER_AMBIGUITY); - } - - @Nullable - private static MethodParameter returnIfQualifiersMatch(MethodParameter pageableParameter, - @Nullable Qualifier assemblerQualifier) { - - if (assemblerQualifier == null) { - return pageableParameter; - } - - Qualifier pageableParameterQualifier = pageableParameter.getParameterAnnotation(Qualifier.class); - - if (pageableParameterQualifier == null) { - return null; - } - - return pageableParameterQualifier.value().equals(assemblerQualifier.value()) ? pageableParameter : null; - } - @Override public boolean supportsParameter(MethodParameter parameter) { return SlicedResourcesAssembler.class.equals(parameter.getParameterType()); @@ -133,13 +55,7 @@ public class SlicedResourcesAssemblerArgumentResolver implements HandlerMethodAr public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) { - MethodParameter pageableParameter = findMatchingPageableParameter(parameter); - - if (pageableParameter != null) { - return new MethodParameterAwareSlicedResourcesAssembler<>(pageableParameter, resolver, null); - } - else { - return new SlicedResourcesAssembler<>(resolver, null); - } + return new SlicedResourcesAssembler<>(resolver, null) // + .withParameter(PageableMethodParameterUtils.findMatchingPageableParameter(parameter)); } } diff --git a/src/main/java/org/springframework/data/web/config/HateoasAwareSpringDataWebConfiguration.java b/src/main/java/org/springframework/data/web/config/HateoasAwareSpringDataWebConfiguration.java index 076d6e0d0..b387698e1 100644 --- a/src/main/java/org/springframework/data/web/config/HateoasAwareSpringDataWebConfiguration.java +++ b/src/main/java/org/springframework/data/web/config/HateoasAwareSpringDataWebConfiguration.java @@ -43,13 +43,14 @@ import org.springframework.web.method.support.HandlerMethodArgumentResolver; * @author Vedran Pavic * @author Mark Paluch * @author Greg Turnquist + * @author Michael Schout */ @Configuration(proxyBeanMethods = false) public class HateoasAwareSpringDataWebConfiguration extends SpringDataWebConfiguration { private final Lazy sortResolver; private final Lazy pageableResolver; - private final Lazy argumentResolver; + private final Lazy pagedResourcesArgumentResolver; private final Lazy slicedResourcesArgumentResolver; /** @@ -65,7 +66,7 @@ public class HateoasAwareSpringDataWebConfiguration extends SpringDataWebConfigu .of(() -> context.getBean("sortResolver", HateoasSortHandlerMethodArgumentResolver.class)); this.pageableResolver = Lazy .of(() -> context.getBean("pageableResolver", HateoasPageableHandlerMethodArgumentResolver.class)); - this.argumentResolver = Lazy.of(() -> context.getBean("pagedResourcesAssemblerArgumentResolver", + this.pagedResourcesArgumentResolver = Lazy.of(() -> context.getBean("pagedResourcesAssemblerArgumentResolver", PagedResourcesAssemblerArgumentResolver.class)); this.slicedResourcesArgumentResolver = Lazy.of(() -> context.getBean("slicedResourcesAssemblerArgumentResolver", SlicedResourcesAssemblerArgumentResolver.class)); @@ -115,7 +116,7 @@ public class HateoasAwareSpringDataWebConfiguration extends SpringDataWebConfigu super.addArgumentResolvers(argumentResolvers); - argumentResolvers.add(argumentResolver.get()); + argumentResolvers.add(pagedResourcesArgumentResolver.get()); argumentResolvers.add(slicedResourcesArgumentResolver.get()); } } diff --git a/src/test/java/org/springframework/data/web/PagedResourcesAssemblerArgumentResolverUnitTests.java b/src/test/java/org/springframework/data/web/PagedResourcesAssemblerArgumentResolverUnitTests.java index 01c9cedf7..72c142aec 100755 --- a/src/test/java/org/springframework/data/web/PagedResourcesAssemblerArgumentResolverUnitTests.java +++ b/src/test/java/org/springframework/data/web/PagedResourcesAssemblerArgumentResolverUnitTests.java @@ -24,6 +24,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.core.MethodParameter; import org.springframework.data.domain.Pageable; +import org.springframework.test.util.ReflectionTestUtils; import org.springframework.web.bind.annotation.RequestMapping; /** @@ -52,7 +53,6 @@ class PagedResourcesAssemblerArgumentResolverUnitTests { var result = resolver.resolveArgument(new MethodParameter(method, 0), null, null, null); assertThat(result).isInstanceOf(PagedResourcesAssembler.class); - assertThat(result).isNotInstanceOf(MethodParameterAwarePagedResourcesAssembler.class); } @Test // DATACMNS-418 @@ -118,10 +118,9 @@ class PagedResourcesAssemblerArgumentResolverUnitTests { private static void assertMethodParameterAwarePagedResourcesAssemblerFor(Object result, MethodParameter parameter) { - assertThat(result).isInstanceOf(MethodParameterAwarePagedResourcesAssembler.class); - var assembler = (MethodParameterAwarePagedResourcesAssembler) result; - - assertThat(assembler.getMethodParameter()).isEqualTo(parameter); + assertThat(result).isInstanceOfSatisfying(PagedResourcesAssembler.class, it -> { + assertThat(ReflectionTestUtils.getField(it, "parameter")).isEqualTo(parameter); + }); } private void assertRejectsAmbiguity(String methodName) throws Exception { diff --git a/src/test/java/org/springframework/data/web/SlicedResourcesAssemblerArgumentResolverUnitTest.java b/src/test/java/org/springframework/data/web/SlicedResourcesAssemblerArgumentResolverUnitTest.java index 1aec6c983..13e359698 100644 --- a/src/test/java/org/springframework/data/web/SlicedResourcesAssemblerArgumentResolverUnitTest.java +++ b/src/test/java/org/springframework/data/web/SlicedResourcesAssemblerArgumentResolverUnitTest.java @@ -1,3 +1,18 @@ +/* + * Copyright 2022-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 static org.assertj.core.api.Assertions.*; @@ -9,81 +24,90 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.core.MethodParameter; import org.springframework.data.domain.Pageable; +import org.springframework.test.util.ReflectionTestUtils; import org.springframework.web.bind.annotation.RequestMapping; +/** + * Unit tests for {@link SlicedResourcesAssemblerArgumentResolver}. + * + * @author Michael Schout + * @author Oliver Drotbohm + * @since 3.1 + */ class SlicedResourcesAssemblerArgumentResolverUnitTest { + SlicedResourcesAssemblerArgumentResolver resolver; - private static void assertMethodParameterAwareSlicedResourcesAssemblerFor(Object result, - MethodParameter parameter) { - assertThat(result).isInstanceOf(MethodParameterAwareSlicedResourcesAssembler.class); - - var assembler = (MethodParameterAwareSlicedResourcesAssembler) result; - - assertThat(assembler.getMethodParameter()).isEqualTo(parameter); - } - @BeforeEach void setUp() { + WebTestUtils.initWebTest(); var hateoasPageableHandlerMethodArgumentResolver = new HateoasPageableHandlerMethodArgumentResolver(); this.resolver = new SlicedResourcesAssemblerArgumentResolver(hateoasPageableHandlerMethodArgumentResolver); } - @Test + @Test // GH-1307 void createsPlainAssemblerWithoutContext() throws Exception { + var method = Controller.class.getMethod("noContext", SlicedResourcesAssembler.class); var result = resolver.resolveArgument(new MethodParameter(method, 0), null, null, null); assertThat(result).isInstanceOf(SlicedResourcesAssembler.class); - assertThat(result).isNotInstanceOf(MethodParameterAwareSlicedResourcesAssembler.class); } - @Test + @Test // GH-1307 void selectsUniquePageableParameter() throws Exception { + var method = Controller.class.getMethod("unique", SlicedResourcesAssembler.class, Pageable.class); + assertSelectsParameter(method, 1); } - @Test + @Test // GH-1307 void selectsUniquePageableParameterForQualifiedAssembler() throws Exception { + var method = Controller.class.getMethod("unnecessarilyQualified", SlicedResourcesAssembler.class, Pageable.class); + assertSelectsParameter(method, 1); } - @Test + @Test // GH-1307 void selectsUniqueQualifiedPageableParameter() throws Exception { var method = Controller.class.getMethod("qualifiedUnique", SlicedResourcesAssembler.class, Pageable.class); + assertSelectsParameter(method, 1); } - @Test + @Test // GH-1307 void selectsQualifiedPageableParameter() throws Exception { + var method = Controller.class.getMethod("qualified", SlicedResourcesAssembler.class, Pageable.class, Pageable.class); + assertSelectsParameter(method, 1); } - @Test + @Test // GH-1307 void rejectsAmbiguousPageableParameters() throws Exception { assertRejectsAmbiguity("unqualifiedAmbiguity"); } - @Test + @Test // GH-1307 void rejectsAmbiguousPageableParametersForQualifiedAssembler() throws Exception { assertRejectsAmbiguity("assemblerQualifiedAmbiguity"); } - @Test + @Test // GH-1307 void rejectsAmbiguityWithoutMatchingQualifiers() throws Exception { assertRejectsAmbiguity("noMatchingQualifiers"); } - @Test + @Test // GH-1307 void doesNotFailForTemplatedMethodMapping() throws Exception { + var method = Controller.class.getMethod("methodWithPathVariable", SlicedResourcesAssembler.class); var result = resolver.resolveArgument(new MethodParameter(method, 0), null, null, null); @@ -91,6 +115,7 @@ class SlicedResourcesAssemblerArgumentResolverUnitTest { } private void assertSelectsParameter(Method method, int expectedIndex) { + var parameter = new MethodParameter(method, 0); var result = resolver.resolveArgument(parameter, null, null, null); @@ -98,13 +123,20 @@ class SlicedResourcesAssemblerArgumentResolverUnitTest { } private void assertRejectsAmbiguity(String methodName) throws Exception { - var method = Controller.class.getMethod(methodName, SlicedResourcesAssembler.class, Pageable.class, - Pageable.class); + + var method = Controller.class.getMethod(methodName, SlicedResourcesAssembler.class, Pageable.class, Pageable.class); assertThatIllegalStateException() .isThrownBy(() -> resolver.resolveArgument(new MethodParameter(method, 0), null, null, null)); } + private static void assertMethodParameterAwareSlicedResourcesAssemblerFor(Object result, MethodParameter parameter) { + + assertThat(result).isInstanceOfSatisfying(SlicedResourcesAssembler.class, it -> { + assertThat(ReflectionTestUtils.getField(it, "parameter")).isEqualTo(parameter); + }); + } + @RequestMapping("/") interface Controller { void noContext(SlicedResourcesAssembler resolver); diff --git a/src/test/java/org/springframework/data/web/SlicedResourcesAssemblerUnitTest.java b/src/test/java/org/springframework/data/web/SlicedResourcesAssemblerUnitTest.java index a0a38da95..b720851d1 100644 --- a/src/test/java/org/springframework/data/web/SlicedResourcesAssemblerUnitTest.java +++ b/src/test/java/org/springframework/data/web/SlicedResourcesAssemblerUnitTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 the original author or authors. + * Copyright 2022-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. @@ -15,8 +15,7 @@ */ package org.springframework.data.web; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.*; import java.net.URI; import java.util.Collection; @@ -26,8 +25,15 @@ import java.util.Map; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.data.domain.*; -import org.springframework.hateoas.*; +import org.springframework.data.domain.AbstractPageRequest; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; +import org.springframework.hateoas.IanaLinkRelations; +import org.springframework.hateoas.Link; +import org.springframework.hateoas.RepresentationModel; +import org.springframework.hateoas.SlicedModel; import org.springframework.hateoas.server.RepresentationModelAssembler; import org.springframework.hateoas.server.core.EmbeddedWrapper; import org.springframework.mock.web.MockHttpServletRequest; @@ -38,37 +44,25 @@ import org.springframework.web.util.UriComponentsBuilder; * Unit tests for {@link SlicedResourcesAssembler}. * * @author Michael Schout + * @author Oliver Drotbohm + * @since 3.1 */ class SlicedResourcesAssemblerUnitTest { + static final Pageable PAGEABLE = PageRequest.of(0, 20); static final Slice EMPTY_SLICE = new SliceImpl<>(Collections.emptyList(), PAGEABLE, false); HateoasPageableHandlerMethodArgumentResolver resolver = new HateoasPageableHandlerMethodArgumentResolver(); SlicedResourcesAssembler assembler = new SlicedResourcesAssembler<>(resolver, null); - private static Slice createSlice(int index) { - Pageable request = PageRequest.of(index, 1); - - var person = new Person(); - person.name = "Dave"; - - boolean hasNext = index < 2; - - return new SliceImpl<>(Collections.singletonList(person), request, hasNext); - } - - private static Map getQueryParameters(Link link) { - var uriComponents = UriComponentsBuilder.fromUri(URI.create(link.expand().getHref())).build(); - return uriComponents.getQueryParams().toSingleValueMap(); - } - @BeforeEach void setUp() { WebTestUtils.initWebTest(); } - @Test + @Test // GH-1307 void addsNextLinkForFirstSlice() { + var resources = assembler.toModel(createSlice(0)); assertThat(resources.getLink(IanaLinkRelations.PREV)).isEmpty(); @@ -76,8 +70,9 @@ class SlicedResourcesAssemblerUnitTest { assertThat(resources.getLink(IanaLinkRelations.NEXT)).isNotEmpty(); } - @Test + @Test // GH-1307 void addsPreviousAndNextLinksForMiddleSlice() { + var resources = assembler.toModel(createSlice(1)); assertThat(resources.getLink(IanaLinkRelations.PREV)).isNotEmpty(); @@ -85,8 +80,9 @@ class SlicedResourcesAssemblerUnitTest { assertThat(resources.getLink(IanaLinkRelations.NEXT)).isNotEmpty(); } - @Test + @Test // GH-1307 void addsPreviousLinkForLastSlice() { + var resources = assembler.toModel(createSlice(2)); assertThat(resources.getLink(IanaLinkRelations.PREV)).isNotEmpty(); @@ -94,8 +90,9 @@ class SlicedResourcesAssemblerUnitTest { assertThat(resources.getLink(IanaLinkRelations.NEXT)).isEmpty(); } - @Test + @Test // GH-1307 void usesBaseUriIfConfigured() { + var baseUri = UriComponentsBuilder.fromUriString("https://foo:9090").build(); var assembler = new SlicedResourcesAssembler(resolver, baseUri); @@ -106,8 +103,9 @@ class SlicedResourcesAssemblerUnitTest { assertThat(resources.getRequiredLink(IanaLinkRelations.NEXT).getHref()).startsWith(baseUri.toUriString()); } - @Test + @Test // GH-1307 void usesCustomLinkProvided() { + var link = Link.of("https://foo:9090", "rel"); var resources = assembler.toModel(createSlice(1), link); @@ -117,8 +115,9 @@ class SlicedResourcesAssemblerUnitTest { assertThat(resources.getRequiredLink(IanaLinkRelations.NEXT).getHref()).startsWith(link.getHref()); } - @Test + @Test // GH-1307 void createsSlicedResourcesForOneIndexedArgumentResolver() { + resolver.setOneIndexedParameters(true); AbstractPageRequest request = PageRequest.of(0, 1); @@ -127,15 +126,17 @@ class SlicedResourcesAssemblerUnitTest { assembler.toModel(slice); } - @Test + @Test // GH-1307 void createsACanonicalLinkWithoutTemplateParameters() { + var resources = assembler.toModel(createSlice(1)); assertThat(resources.getRequiredLink(IanaLinkRelations.SELF).getHref()).doesNotContain("{").doesNotContain("}"); } - @Test + @Test // GH-1307 void invokesCustomElementResourceAssembler() { + var personAssembler = new PersonResourceAssembler(); var resources = assembler.toModel(createSlice(0), personAssembler); @@ -148,8 +149,9 @@ class SlicedResourcesAssemblerUnitTest { assertThat(content.iterator().next().name).isEqualTo("Dave"); } - @Test + @Test // GH-1307 void createsPaginationLinksForOneIndexedArgumentResolverCorrectly() { + var argumentResolver = new HateoasPageableHandlerMethodArgumentResolver(); argumentResolver.setOneIndexedParameters(true); @@ -167,8 +169,9 @@ class SlicedResourcesAssemblerUnitTest { assertThat(getQueryParameters(resource.getRequiredLink("next"))).containsEntry("page", "3"); } - @Test + @Test // GH-1307 void generatedLinksShouldNotBeTemplated() { + var resources = assembler.toModel(createSlice(1)); assertThat(resources.getRequiredLink(IanaLinkRelations.SELF).getHref()).doesNotContain("{").doesNotContain("}"); @@ -176,8 +179,9 @@ class SlicedResourcesAssemblerUnitTest { assertThat(resources.getRequiredLink(IanaLinkRelations.PREV).getHref()).endsWith("?page=0&size=1"); } - @Test + @Test // GH-1307 void generatesEmptySliceResourceWithEmbeddedWrapper() { + var result = assembler.toEmptyModel(EMPTY_SLICE, Person.class); var content = result.getContent(); @@ -188,39 +192,40 @@ class SlicedResourcesAssemblerUnitTest { assertThat(((EmbeddedWrapper) element).getRelTargetType()).isEqualTo(Person.class); } - @Test + @Test // GH-1307 void emptySliceCreatorRejectsSliceWithContent() { assertThatIllegalArgumentException().isThrownBy(() -> assembler.toEmptyModel(createSlice(1), Person.class)); } - @Test + @Test // GH-1307 void emptySliceCreatorRejectsNullType() { assertThatIllegalArgumentException().isThrownBy(() -> assembler.toEmptyModel(EMPTY_SLICE, null)); } - @Test + @Test // GH-1307 void addsFirstLinkForMultipleSlices() { var resources = assembler.toModel(createSlice(1)); assertThat(resources.getRequiredLink(IanaLinkRelations.FIRST).getHref()).endsWith("?page=0&size=1"); } - @Test + @Test // GH-1307 void addsFirstLinkForFirstSlice() { var resources = assembler.toModel(createSlice(0)); assertThat(resources.getRequiredLink(IanaLinkRelations.FIRST).getHref()).endsWith("?page=0&size=1"); } - @Test + @Test // GH-1307 void addsFirstLinkForLastSlice() { var resources = assembler.toModel(createSlice(2)); assertThat(resources.getRequiredLink(IanaLinkRelations.FIRST).getHref()).endsWith("?page=0&size=1"); } - @Test + @Test // GH-1307 void alwaysAddsFirstLinkIfConfiguredTo() { + var assembler = new SlicedResourcesAssembler(resolver, null); assembler.setForceFirstRel(true); @@ -229,22 +234,22 @@ class SlicedResourcesAssemblerUnitTest { assertThat(resources.getRequiredLink(IanaLinkRelations.FIRST).getHref()).endsWith("?page=0&size=20"); } - @Test + @Test // GH-1307 void usesCustomSlicedResources() { - RepresentationModelAssembler, SlicedModel>> assembler = new CustomSlicedResourcesAssembler<>( - resolver, null); + + var assembler = new CustomSlicedResourcesAssembler(resolver, null); assertThat(assembler.toModel(EMPTY_SLICE)).isInstanceOf(CustomSlicedResources.class); } - @Test + @Test // GH-1307 void selfLinkContainsCoordinatesForCurrentSlice() { var resource = assembler.toModel(createSlice(0)); assertThat(resource.getRequiredLink(IanaLinkRelations.SELF).getHref()).endsWith("?page=0&size=1"); } - @Test + @Test // GH-1307 void keepsRequestParametersOfOriginalRequestUri() { WebTestUtils.initWebTest(new MockHttpServletRequest("GET", "/sample?foo=bar")); @@ -254,6 +259,23 @@ class SlicedResourcesAssemblerUnitTest { .isEqualTo("http://localhost/sample?foo=bar&page=0&size=1"); } + private static Slice createSlice(int index) { + + Pageable request = PageRequest.of(index, 1); + + var person = new Person(); + person.name = "Dave"; + + boolean hasNext = index < 2; + + return new SliceImpl<>(Collections.singletonList(person), request, hasNext); + } + + private static Map getQueryParameters(Link link) { + var uriComponents = UriComponentsBuilder.fromUri(URI.create(link.expand().getHref())).build(); + return uriComponents.getQueryParams().toSingleValueMap(); + } + static class Person { String name; } @@ -288,4 +310,4 @@ class SlicedResourcesAssemblerUnitTest { super(content, metadata); } } -} \ No newline at end of file +}