Polishing.
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.
This commit is contained in:
@@ -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<T> extends PagedResourcesAssembler<T> {
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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<T> extends SlicedResourcesAssembler<T> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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<MethodParameter> 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;
|
||||
}
|
||||
}
|
||||
@@ -58,6 +58,7 @@ public class PagedResourcesAssembler<T> 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<T> implements RepresentationModelAssembler<
|
||||
*/
|
||||
public PagedResourcesAssembler(@Nullable HateoasPageableHandlerMethodArgumentResolver resolver,
|
||||
@Nullable UriComponents baseUri) {
|
||||
this(resolver, Optional.ofNullable(baseUri), null);
|
||||
}
|
||||
|
||||
private PagedResourcesAssembler(@Nullable HateoasPageableHandlerMethodArgumentResolver resolver,
|
||||
Optional<UriComponents> 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<T> 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<T> withParameter(@Nullable MethodParameter parameter) {
|
||||
return new PagedResourcesAssembler<>(pageableResolver, baseUri, parameter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PagedModel<EntityModel<T>> toModel(Page<T> entity) {
|
||||
return toModel(entity, EntityModel::of);
|
||||
@@ -267,7 +285,7 @@ public class PagedResourcesAssembler<T> 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<T> 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;
|
||||
}
|
||||
|
||||
@@ -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<MethodParameter> 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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<T>
|
||||
implements RepresentationModelAssembler<Slice<T>, SlicedModel<EntityModel<T>>> {
|
||||
|
||||
private final HateoasPageableHandlerMethodArgumentResolver pageableResolver;
|
||||
|
||||
private final Optional<UriComponents> 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<UriComponents> 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<T> withParameter(@Nullable MethodParameter parameter) {
|
||||
return new SlicedResourcesAssembler<>(pageableResolver, baseUri, parameter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SlicedModel<EntityModel<T>> toModel(Slice<T> 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<T>
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<T>
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<T>
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<T>
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<T>
|
||||
}
|
||||
|
||||
public SlicedModel<?> toEmptyModel(Slice<?> slice, Class<?> type, Optional<Link> 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<T>
|
||||
/**
|
||||
* 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<T>
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<T>
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<T>
|
||||
*/
|
||||
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<T>
|
||||
private String baseUriOrCurrentRequest() {
|
||||
return baseUri.map(Object::toString).orElseGet(SlicedResourcesAssembler::currentRequest);
|
||||
}
|
||||
}
|
||||
|
||||
private static String currentRequest() {
|
||||
return ServletUriComponentsBuilder.fromCurrentRequest().build().toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<MethodParameter> 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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<HateoasSortHandlerMethodArgumentResolver> sortResolver;
|
||||
private final Lazy<HateoasPageableHandlerMethodArgumentResolver> pageableResolver;
|
||||
private final Lazy<PagedResourcesAssemblerArgumentResolver> argumentResolver;
|
||||
private final Lazy<PagedResourcesAssemblerArgumentResolver> pagedResourcesArgumentResolver;
|
||||
private final Lazy<SlicedResourcesAssemblerArgumentResolver> 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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<Object> resolver);
|
||||
|
||||
@@ -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<Person> EMPTY_SLICE = new SliceImpl<>(Collections.emptyList(), PAGEABLE, false);
|
||||
|
||||
HateoasPageableHandlerMethodArgumentResolver resolver = new HateoasPageableHandlerMethodArgumentResolver();
|
||||
SlicedResourcesAssembler<Person> assembler = new SlicedResourcesAssembler<>(resolver, null);
|
||||
|
||||
private static Slice<Person> 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<String, String> 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<Person>(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<Person>(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<Slice<Person>, SlicedModel<EntityModel<Person>>> assembler = new CustomSlicedResourcesAssembler<>(
|
||||
resolver, null);
|
||||
|
||||
var assembler = new CustomSlicedResourcesAssembler<Person>(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<Person> 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<String, String> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user