From 116730c168013790762c86f8dc70a7b955712e1b Mon Sep 17 00:00:00 2001 From: Oliver Drotbohm Date: Tue, 28 Feb 2023 17:00:37 +0100 Subject: [PATCH] Polishing. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Imports, Javadoc, ticket references in test cases. Removed the need for MethodParameterAware… flavors of the ResourceAssemblers by keeping the MethodParameter reference in the original assemblers in the first place. Extracted common Pageable MethodParameter lookup code into PageableMethodParameterUtils. Related ticket: #1307. --- ...ParameterAwarePagedResourcesAssembler.java | 56 --------- ...arameterAwareSlicedResourcesAssembler.java | 55 --------- .../web/PageableMethodParameterUtils.java | 111 +++++++++++++++++ .../data/web/PagedResourcesAssembler.java | 24 +++- ...gedResourcesAssemblerArgumentResolver.java | 94 +------------- .../data/web/SlicedResourcesAssembler.java | 115 +++++++++--------- ...cedResourcesAssemblerArgumentResolver.java | 94 +------------- ...ateoasAwareSpringDataWebConfiguration.java | 7 +- ...cesAssemblerArgumentResolverUnitTests.java | 9 +- ...rcesAssemblerArgumentResolverUnitTest.java | 74 +++++++---- .../web/SlicedResourcesAssemblerUnitTest.java | 110 ++++++++++------- 11 files changed, 327 insertions(+), 422 deletions(-) delete mode 100644 src/main/java/org/springframework/data/web/MethodParameterAwarePagedResourcesAssembler.java delete mode 100644 src/main/java/org/springframework/data/web/MethodParameterAwareSlicedResourcesAssembler.java create mode 100644 src/main/java/org/springframework/data/web/PageableMethodParameterUtils.java 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 +}