Browse Source

DATACMNS-236, DATACMNS-117 - Added (Pageable|Sort)HanderMethodArgumentResolvers.

Added HandlerMethodArgumentResolver implementations for Pageable and Sort exposing a new default set of properties (page, size, sort) to resolve pagination and sorting information from the request. To mimic the legacy behavior we expose a (deprecated) LEGACY constant in PageableHandlerMethodArgumentResolver. Clients should move to the new properties structure ASAP. Added unit tests to verify old and new defaulting behavior.

Introduced new annotations @SortDefault (with @SortDefaults wrapper annotation) and @PageableDefault (superseding the legacy @PageableDefaults). The new annotations have more speaking attribute names and the HMAR implementations transparently alias the generic value attribute into a more semantic one (size).

The HMAR implementations implement Spring HATEOAS' UriComponentsContributor to be able to turn Pageable / Sort instances back into URIs created through the MethodLinkBuilderFactory API in Spring HATEOAS.

Extracted common API between legacy PageableArgumentResolver and PageableHandlerMethodArgumentResolver into common helper class.
Upgraded to Spring Hateoas 0.5.0.BUILD-SNAPSHOT.
pull/25/head
Oliver Gierke 13 years ago
parent
commit
e11efede4d
  1. 2
      pom.xml
  2. 95
      src/main/java/org/springframework/data/web/PageableArgumentResolver.java
  3. 73
      src/main/java/org/springframework/data/web/PageableDefault.java
  4. 4
      src/main/java/org/springframework/data/web/PageableDefaults.java
  5. 295
      src/main/java/org/springframework/data/web/PageableHandlerArgumentResolver.java
  6. 255
      src/main/java/org/springframework/data/web/PageableHandlerMethodArgumentResolver.java
  7. 78
      src/main/java/org/springframework/data/web/SortDefault.java
  8. 406
      src/main/java/org/springframework/data/web/SortHandlerMethodArgumentResolver.java
  9. 142
      src/main/java/org/springframework/data/web/SpringDataAnnotationUtils.java
  10. 113
      src/test/java/org/springframework/data/web/LegacyPageableHandlerArgumentResolverUnitTests.java
  11. 5
      src/test/java/org/springframework/data/web/PageableArgumentResolverUnitTests.java
  12. 155
      src/test/java/org/springframework/data/web/PageableDefaultUnitTest.java
  13. 76
      src/test/java/org/springframework/data/web/PageableHandlerMethodArgumentResolverUnitTest.java
  14. 143
      src/test/java/org/springframework/data/web/SortDefaultUnitTest.java
  15. 164
      src/test/java/org/springframework/data/web/SortHandlerArgumentResolverUnitTests.java
  16. 51
      src/test/java/org/springframework/data/web/TestUtils.java

2
pom.xml

@ -19,7 +19,7 @@ @@ -19,7 +19,7 @@
<properties>
<jackson>1.9.7</jackson>
<querydsl>2.8.0</querydsl>
<springhateoas>0.4.0.RELEASE</springhateoas>
<springhateoas>0.5.0.BUILD-SNAPSHOT</springhateoas>
<dist.key>DATACMNS</dist.key>
</properties>

95
src/main/java/org/springframework/data/web/PageableArgumentResolver.java

@ -15,11 +15,10 @@ @@ -15,11 +15,10 @@
*/
package org.springframework.data.web;
import static org.springframework.data.web.SpringDataAnnotationUtils.*;
import java.beans.PropertyEditorSupport;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.Set;
import javax.servlet.ServletRequest;
@ -131,7 +130,7 @@ public class PageableArgumentResolver implements WebArgumentResolver { @@ -131,7 +130,7 @@ public class PageableArgumentResolver implements WebArgumentResolver {
return new PageRequest(fallbackPagable.getPageNumber(), fallbackPagable.getPageSize(), fallbackPagable.getSort());
}
private static Pageable getDefaultPageRequestFrom(PageableDefaults defaults) {
static Pageable getDefaultPageRequestFrom(PageableDefaults defaults) {
// +1 is because we substract 1 later
int defaultPageNumber = defaults.pageNumber() + 1;
@ -162,94 +161,6 @@ public class PageableArgumentResolver implements WebArgumentResolver { @@ -162,94 +161,6 @@ public class PageableArgumentResolver implements WebArgumentResolver {
return prefix;
}
/**
* Asserts uniqueness of all {@link Pageable} parameters of the method of the given {@link MethodParameter}.
*
* @param parameter
*/
private void assertPageableUniqueness(MethodParameter parameter) {
Method method = parameter.getMethod();
if (containsMoreThanOnePageableParameter(method)) {
Annotation[][] annotations = method.getParameterAnnotations();
assertQualifiersFor(method.getParameterTypes(), annotations);
}
}
/**
* Returns whether the given {@link Method} has more than one {@link Pageable} parameter.
*
* @param method
* @return
*/
private boolean containsMoreThanOnePageableParameter(Method method) {
boolean pageableFound = false;
for (Class<?> type : method.getParameterTypes()) {
if (pageableFound && type.equals(Pageable.class)) {
return true;
}
if (type.equals(Pageable.class)) {
pageableFound = true;
}
}
return false;
}
/**
* Asserts that every {@link Pageable} parameter of the given parameters carries an {@link Qualifier} annotation to
* distinguish them from each other.
*
* @param parameterTypes
* @param annotations
*/
private void assertQualifiersFor(Class<?>[] parameterTypes, Annotation[][] annotations) {
Set<String> values = new HashSet<String>();
for (int i = 0; i < annotations.length; i++) {
if (Pageable.class.equals(parameterTypes[i])) {
Qualifier qualifier = findAnnotation(annotations[i]);
if (null == qualifier) {
throw new IllegalStateException(
"Ambiguous Pageable arguments in handler method. If you use multiple parameters of type Pageable you need to qualify them with @Qualifier");
}
if (values.contains(qualifier.value())) {
throw new IllegalStateException("Values of the user Qualifiers must be unique!");
}
values.add(qualifier.value());
}
}
}
/**
* Returns a {@link Qualifier} annotation from the given array of {@link Annotation}s. Returns {@literal null} if the
* array does not contain a {@link Qualifier} annotation.
*
* @param annotations
* @return
*/
private Qualifier findAnnotation(Annotation[] annotations) {
for (Annotation annotation : annotations) {
if (annotation instanceof Qualifier) {
return (Qualifier) annotation;
}
}
return null;
}
/**
* {@link java.beans.PropertyEditor} to create {@link Sort} instances from textual representations. The implementation
* interprets the string as a comma separated list where the first entry is the sort direction ( {@code asc},

73
src/main/java/org/springframework/data/web/PageableDefault.java

@ -0,0 +1,73 @@ @@ -0,0 +1,73 @@
/*
* Copyright 2013 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
*
* http://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.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.web.SortDefault.SortDefaults;
/**
* Annotation to set defaults when injecting a {@link org.springframework.data.domain.Pageable} into a controller
* method. Instead of configuring {@link #sort()} and {@link #direction()} you can also use {@link SortDefault} or
* {@link SortDefaults}.
*
* @since 1.6
* @author Oliver Gierke
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface PageableDefault {
/**
* Alias for {@link #size()}. Prefer to use the {@link #size()} method as it makes the annotation declaration more
* expressive and you'll probably want to configure the {@link #page()} anyway.
*
* @return
*/
int value() default 10;
/**
* The default-size the injected {@link org.springframework.data.domain.Pageable} should get if no corresponding
* parameter defined in request (default is 10).
*/
int size() default 10;
/**
* The default-pagenumber the injected {@link org.springframework.data.domain.Pageable} should get if no corresponding
* parameter defined in request (default is 0).
*/
int page() default 0;
/**
* The properties to sort by by default. If unset, no sorting will be applied at all.
*
* @return
*/
String[] sort() default {};
/**
* The direction to sort by. Defaults to {@link Direction#ASC}.
*
* @return
*/
Direction direction() default Direction.ASC;
}

4
src/main/java/org/springframework/data/web/PageableDefaults.java

@ -15,6 +15,7 @@ @@ -15,6 +15,7 @@
*/
package org.springframework.data.web;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@ -26,10 +27,13 @@ import org.springframework.data.domain.Sort.Direction; @@ -26,10 +27,13 @@ import org.springframework.data.domain.Sort.Direction;
* Annotation to set defaults when injecting a {@link org.springframework.data.domain.Pageable} into a controller
* method.
*
* @deprecated use {@link PageableDefault} instead.
* @author Oliver Gierke
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@Deprecated
public @interface PageableDefaults {
/**

295
src/main/java/org/springframework/data/web/PageableHandlerArgumentResolver.java

@ -1,295 +0,0 @@ @@ -1,295 +0,0 @@
/*
* Copyright 2013 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
*
* http://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.beans.PropertyEditorSupport;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.Set;
import javax.servlet.ServletRequest;
import org.springframework.beans.PropertyValue;
import org.springframework.beans.PropertyValues;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.MethodParameter;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.validation.DataBinder;
import org.springframework.web.bind.ServletRequestDataBinder;
import org.springframework.web.bind.ServletRequestParameterPropertyValues;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
/**
* Extracts paging information from web requests and thus allows injecting {@link Pageable} instances into controller
* methods. Request properties to be parsed can be configured. Default configuration uses request properties beginning
* with {@link #DEFAULT_PREFIX}{@link #DEFAULT_SEPARATOR}.
*
* @since 1.6
* @author Oliver Gierke
*/
public class PageableHandlerArgumentResolver implements HandlerMethodArgumentResolver {
private static final Pageable DEFAULT_PAGE_REQUEST = new PageRequest(0, 10);
private static final String DEFAULT_PREFIX = "page";
private static final String DEFAULT_SEPARATOR = ".";
private Pageable fallbackPagable = DEFAULT_PAGE_REQUEST;
private String prefix = DEFAULT_PREFIX;
private String separator = DEFAULT_SEPARATOR;
/**
* Setter to configure a fallback instance of {@link Pageable} that is being used to back missing parameters. Defaults
* to {@link #DEFAULT_PAGE_REQUEST}.
*
* @param fallbackPagable the fallbackPagable to set
*/
public void setFallbackPagable(Pageable fallbackPagable) {
this.fallbackPagable = null == fallbackPagable ? DEFAULT_PAGE_REQUEST : fallbackPagable;
}
/**
* Setter to configure the prefix of request parameters to be used to retrieve paging information. Defaults to
* {@link #DEFAULT_PREFIX}.
*
* @param prefix the prefix to set
*/
public void setPrefix(String prefix) {
this.prefix = null == prefix ? DEFAULT_PREFIX : prefix;
}
/**
* Setter to configure the separator between prefix and actual property value. Defaults to {@link #DEFAULT_SEPARATOR}.
*
* @param separator the separator to set. Will default to {@link #DEFAULT_SEPEARATOR} if set to {@literal null}.
*/
public void setSeparator(String separator) {
this.separator = null == separator ? DEFAULT_SEPARATOR : separator;
}
/*
* (non-Javadoc)
* @see org.springframework.web.method.support.HandlerMethodArgumentResolver#supportsParameter(org.springframework.core.MethodParameter)
*/
public boolean supportsParameter(MethodParameter parameter) {
return Pageable.class.equals(parameter.getParameterType());
}
/*
* (non-Javadoc)
* @see org.springframework.web.method.support.HandlerMethodArgumentResolver#resolveArgument(org.springframework.core.MethodParameter, org.springframework.web.method.support.ModelAndViewContainer, org.springframework.web.context.request.NativeWebRequest, org.springframework.web.bind.support.WebDataBinderFactory)
*/
public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
assertPageableUniqueness(methodParameter);
Pageable request = getDefaultFromAnnotationOrFallback(methodParameter);
ServletRequest servletRequest = (ServletRequest) webRequest.getNativeRequest();
PropertyValues propertyValues = new ServletRequestParameterPropertyValues(servletRequest,
getPrefix(methodParameter), separator);
DataBinder binder = new ServletRequestDataBinder(request);
binder.initDirectFieldAccess();
binder.registerCustomEditor(Sort.class, new SortPropertyEditor("sort.dir", propertyValues));
binder.bind(propertyValues);
if (request.getPageNumber() > 0) {
request = new PageRequest(request.getPageNumber() - 1, request.getPageSize(), request.getSort());
}
return request;
}
private Pageable getDefaultFromAnnotationOrFallback(MethodParameter methodParameter) {
// search for PageableDefaults annotation
for (Annotation annotation : methodParameter.getParameterAnnotations()) {
if (annotation instanceof PageableDefaults) {
return getDefaultPageRequestFrom((PageableDefaults) annotation);
}
}
// Construct request with fallback request to ensure sensible
// default values. Create fresh copy as Spring will manipulate the
// instance under the covers
return new PageRequest(fallbackPagable.getPageNumber(), fallbackPagable.getPageSize(), fallbackPagable.getSort());
}
private static Pageable getDefaultPageRequestFrom(PageableDefaults defaults) {
// +1 is because we substract 1 later
int defaultPageNumber = defaults.pageNumber() + 1;
int defaultPageSize = defaults.value();
if (defaults.sort().length == 0) {
return new PageRequest(defaultPageNumber, defaultPageSize);
}
return new PageRequest(defaultPageNumber, defaultPageSize, defaults.sortDir(), defaults.sort());
}
/**
* Resolves the prefix to use to bind properties from. Will prepend a possible {@link Qualifier} if available or
* return the configured prefix otherwise.
*
* @param parameter
* @return
*/
private String getPrefix(MethodParameter parameter) {
for (Annotation annotation : parameter.getParameterAnnotations()) {
if (annotation instanceof Qualifier) {
return new StringBuilder(((Qualifier) annotation).value()).append("_").append(prefix).toString();
}
}
return prefix;
}
/**
* Asserts uniqueness of all {@link Pageable} parameters of the method of the given {@link MethodParameter}.
*
* @param parameter
*/
private void assertPageableUniqueness(MethodParameter parameter) {
Method method = parameter.getMethod();
if (containsMoreThanOnePageableParameter(method)) {
Annotation[][] annotations = method.getParameterAnnotations();
assertQualifiersFor(method.getParameterTypes(), annotations);
}
}
/**
* Returns whether the given {@link Method} has more than one {@link Pageable} parameter.
*
* @param method
* @return
*/
private boolean containsMoreThanOnePageableParameter(Method method) {
boolean pageableFound = false;
for (Class<?> type : method.getParameterTypes()) {
if (pageableFound && type.equals(Pageable.class)) {
return true;
}
if (type.equals(Pageable.class)) {
pageableFound = true;
}
}
return false;
}
/**
* Asserts that every {@link Pageable} parameter of the given parameters carries an {@link Qualifier} annotation to
* distinguish them from each other.
*
* @param parameterTypes
* @param annotations
*/
private void assertQualifiersFor(Class<?>[] parameterTypes, Annotation[][] annotations) {
Set<String> values = new HashSet<String>();
for (int i = 0; i < annotations.length; i++) {
if (Pageable.class.equals(parameterTypes[i])) {
Qualifier qualifier = findAnnotation(annotations[i]);
if (null == qualifier) {
throw new IllegalStateException(
"Ambiguous Pageable arguments in handler method. If you use multiple parameters of type Pageable you need to qualify them with @Qualifier");
}
if (values.contains(qualifier.value())) {
throw new IllegalStateException("Values of the user Qualifiers must be unique!");
}
values.add(qualifier.value());
}
}
}
/**
* Returns a {@link Qualifier} annotation from the given array of {@link Annotation}s. Returns {@literal null} if the
* array does not contain a {@link Qualifier} annotation.
*
* @param annotations
* @return
*/
private Qualifier findAnnotation(Annotation[] annotations) {
for (Annotation annotation : annotations) {
if (annotation instanceof Qualifier) {
return (Qualifier) annotation;
}
}
return null;
}
/**
* {@link java.beans.PropertyEditor} to create {@link Sort} instances from textual representations. The implementation
* interprets the string as a comma separated list where the first entry is the sort direction ( {@code asc},
* {@code desc}) followed by the properties to sort by.
*
* @author Oliver Gierke
*/
private static class SortPropertyEditor extends PropertyEditorSupport {
private final String orderProperty;
private final PropertyValues values;
/**
* Creates a new {@link SortPropertyEditor}.
*
* @param orderProperty
* @param values
*/
public SortPropertyEditor(String orderProperty, PropertyValues values) {
this.orderProperty = orderProperty;
this.values = values;
}
/*
* (non-Javadoc)
* @see java.beans.PropertyEditorSupport#setAsText(java.lang.String)
*/
@Override
public void setAsText(String text) {
PropertyValue rawOrder = values.getPropertyValue(orderProperty);
Direction order = null == rawOrder ? Direction.ASC : Direction.fromString(rawOrder.getValue().toString());
setValue(new Sort(order, text));
}
}
}

255
src/main/java/org/springframework/data/web/PageableHandlerMethodArgumentResolver.java

@ -0,0 +1,255 @@ @@ -0,0 +1,255 @@
/*
* Copyright 2013 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
*
* http://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.springframework.data.web.SpringDataAnnotationUtils.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.MethodParameter;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.hateoas.mvc.UriComponentsContributor;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.util.UriComponentsBuilder;
/**
* Extracts paging information from web requests and thus allows injecting {@link Pageable} instances into controller
* methods. Request properties to be parsed can be configured. Default configuration uses request properties beginning
* with {@link #PAGE_PROPERTY}{@link #DEFAULT_SEPARATOR}.
*
* @since 1.6
* @author Oliver Gierke
*/
@SuppressWarnings("deprecation")
public class PageableHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver, UriComponentsContributor {
/**
* A {@link PageableHandlerMethodArgumentResolver} preconfigured to the setup of {@link PageableArgumentResolver}. Use
* that if you need to stick to the former request parameters an 1-indexed behavior. This will be removed in the next
* major version (1.7). So consider migrating to the new way of exposing request parameters.
*/
@Deprecated
public static final PageableHandlerMethodArgumentResolver LEGACY;
static {
LEGACY = new PageableHandlerMethodArgumentResolver();
LEGACY.pageProperty = "page.page";
LEGACY.sizeProperty = "page.size";
LEGACY.fallbackPageable = new PageRequest(1, 10);
LEGACY.oneIndexedParameters = true;
LEGACY.sortResolver.setLegacyMode(true);
LEGACY.sortResolver.setSortParameter("page.sort");
}
private static final Pageable DEFAULT_PAGE_REQUEST = new PageRequest(0, 20);
private static final String DEFAULT_PAGE_PROPERTY = "page";
private static final String DEFAULT_SIZE_PROPERTY = "size";
private static final String DEFAULT_PREFIX = "";
private static final String DEFAULT_QUALIFIER_SEPARATOR = "_";
private Pageable fallbackPageable = DEFAULT_PAGE_REQUEST;
private SortHandlerMethodArgumentResolver sortResolver = new SortHandlerMethodArgumentResolver();
private String pageProperty = DEFAULT_PAGE_PROPERTY;
private String sizeProperty = DEFAULT_SIZE_PROPERTY;
private String prefix = DEFAULT_PREFIX;
private String qualifierSeparator = DEFAULT_QUALIFIER_SEPARATOR;
private boolean oneIndexedParameters = false;
/**
* Configures the {@link Pageable} to be used as fallback in case no {@link PageableDefault} or
* {@link PageableDefaults} (the latter only supported in legacy mode) can be found at the method parameter to be
* resolved.
* <p>
* If you set this to {@literal null}, be aware that you controller methods will get {@literal null} handed into them
* in case no {@link Pageable} data can be found in the request.
*
* @param fallbackPageable the {@link Pageable} to be used as general fallback.
*/
public void setFallbackPageable(Pageable fallbackPageable) {
this.fallbackPageable = fallbackPageable;
}
/**
* Configures the parameter name to be used to find the page number in the request. Defaults to {@code page}.
*
* @param pageProperty the parameter name to be used, must not be {@literal null} or empty.
*/
public void setPageProperty(String pageProperty) {
Assert.hasText(pageProperty, "Page parameter name must not be null or empty!");
this.pageProperty = pageProperty;
}
/**
* Configures the parameter name to be used to find the page size in the request. Defaults to {@code size}.
*
* @param sizeProperty the parameter name to be used, must not be {@literal null} or empty.
*/
public void setSizeProperty(String sizeProperty) {
Assert.hasText(sizeProperty, "Size parameter name must not be null or empty!");
this.sizeProperty = sizeProperty;
}
/**
* Configures a general prefix to be prepended to the page number and page size parameters. Useful to namespace the
* property names used in case they are clashing with ones used by your application. By default, no prefix is used.
*
* @param prefix the prefix to be used or {@literal null} to reset to the default.
*/
public void setPrefix(String prefix) {
this.prefix = prefix == null ? DEFAULT_PREFIX : prefix;
}
/**
* The separator to be used between the qualifier and the actual page number and size properties. Defaults to
* {@code _}. So a qualifier of {@code foo} will result in a page number parameter of {@code foo_page}.
*
* @param qualifierSeparator the qualifierSeparator to be used or {@literal null} to reset to the default.
*/
public void setQualifierSeparator(String qualifierSeparator) {
this.qualifierSeparator = qualifierSeparator == null ? DEFAULT_QUALIFIER_SEPARATOR : qualifierSeparator;
}
/**
* Configure the {@link SortHandlerMethodArgumentResolver} to be used with the
* {@link PageableHandlerMethodArgumentResolver}.
*
* @param sortResolver the {@link SortHandlerMethodArgumentResolver} to be used ot {@literal null} to reset it to the
* default one.
*/
public void setSortResolver(SortHandlerMethodArgumentResolver sortResolver) {
this.sortResolver = sortResolver == null ? new SortHandlerMethodArgumentResolver() : sortResolver;
}
/**
* Configures whether to expose and assume 1-based page number indexes in the request parameters. Defaults to
* {@literal false}, meaning a page number of 0 in the request equals the first page. If this is set to
* {@literal true}, a page number of 1 in the request will be considered the first page.
*
* @param oneIndexedParameters the oneIndexedParameters to set
*/
public void setOneIndexedParameters(boolean oneIndexedParameters) {
this.oneIndexedParameters = oneIndexedParameters;
}
/*
* (non-Javadoc)
* @see org.springframework.web.method.support.HandlerMethodArgumentResolver#supportsParameter(org.springframework.core.MethodParameter)
*/
public boolean supportsParameter(MethodParameter parameter) {
return Pageable.class.equals(parameter.getParameterType());
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.mvc.UriComponentsContributor#enhance(org.springframework.web.util.UriComponentsBuilder, org.springframework.core.MethodParameter, java.lang.Object)
*/
public void enhance(UriComponentsBuilder builder, MethodParameter parameter, Object value) {
if (!(value instanceof Pageable)) {
return;
}
Pageable pageable = (Pageable) value;
String pagePropertyName = getParameterNameToUse(pageProperty, parameter);
String propertyToLookup = getParameterNameToUse(sizeProperty, parameter);
int pageNumber = pageable.getPageNumber();
builder.queryParam(pagePropertyName, oneIndexedParameters ? pageNumber + 1 : pageNumber);
builder.queryParam(propertyToLookup, pageable.getPageSize());
sortResolver.enhance(builder, parameter, pageable.getSort());
}
/*
* (non-Javadoc)
* @see org.springframework.web.method.support.HandlerMethodArgumentResolver#resolveArgument(org.springframework.core.MethodParameter, org.springframework.web.method.support.ModelAndViewContainer, org.springframework.web.context.request.NativeWebRequest, org.springframework.web.bind.support.WebDataBinderFactory)
*/
public Pageable resolveArgument(MethodParameter methodParameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
assertPageableUniqueness(methodParameter);
Pageable defaultOrFallback = getDefaultFromAnnotationOrFallback(methodParameter);
String pageString = webRequest.getParameter(getParameterNameToUse(pageProperty, methodParameter));
String pageSizeString = webRequest.getParameter(getParameterNameToUse(sizeProperty, methodParameter));
int page = StringUtils.hasText(pageString) ? Integer.parseInt(pageString) - (oneIndexedParameters ? 1 : 0)
: defaultOrFallback.getPageNumber();
int pageSize = StringUtils.hasText(pageSizeString) ? Integer.parseInt(pageSizeString) : defaultOrFallback
.getPageSize();
Sort sort = sortResolver.resolveArgument(methodParameter, mavContainer, webRequest, binderFactory);
return new PageRequest(page, pageSize, sort == null ? defaultOrFallback.getSort() : sort);
}
/**
* Returns the name of the request parameter to find the {@link Pageable} information in. Inspects the given
* {@link MethodParameter} for {@link Qualifier} present and prefixes the given source parameter name with it.
*
* @param source the basic parameter name.
* @param parameter the {@link MethodParameter} potentially qualified.
* @return
*/
private String getParameterNameToUse(String source, MethodParameter parameter) {
StringBuilder builder = new StringBuilder(prefix);
if (parameter.hasParameterAnnotation(Qualifier.class)) {
builder.append(parameter.getParameterAnnotation(Qualifier.class).value());
builder.append(qualifierSeparator);
}
return builder.append(source).toString();
}
private Pageable getDefaultFromAnnotationOrFallback(MethodParameter methodParameter) {
if (sortResolver.legacyMode && methodParameter.hasParameterAnnotation(PageableDefaults.class)) {
Pageable pageable = PageableArgumentResolver.getDefaultPageRequestFrom(methodParameter
.getParameterAnnotation(PageableDefaults.class));
return new PageRequest(pageable.getPageNumber() - 1, pageable.getPageSize(), pageable.getSort());
}
if (methodParameter.hasParameterAnnotation(PageableDefault.class)) {
return getDefaultPageRequestFrom(methodParameter.getParameterAnnotation(PageableDefault.class));
}
return fallbackPageable;
}
private static Pageable getDefaultPageRequestFrom(PageableDefault defaults) {
int defaultPageNumber = defaults.page();
int defaultPageSize = getSpecificPropertyOrDefaultFromValue(defaults, "size");
if (defaults.sort().length == 0) {
return new PageRequest(defaultPageNumber, defaultPageSize);
}
return new PageRequest(defaultPageNumber, defaultPageSize, defaults.direction(), defaults.sort());
}
}

78
src/main/java/org/springframework/data/web/SortDefault.java

@ -0,0 +1,78 @@ @@ -0,0 +1,78 @@
/*
* Copyright 2013 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
*
* http://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.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
/**
* Annotation to define the default {@link Sort} options to be used when injecting a {@link Sort} instance into a
* controller handler method.
*
* @since 1.6
* @author Oliver Gierke
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface SortDefault {
/**
* Alias for {@link #sort()} to make a declaration configuring fields only more concise.
*
* @return
*/
String[] value() default {};
/**
* The properties to sort by by default. If unset, no sorting will be applied at all.
*
* @return
*/
String[] sort() default {};
/**
* The direction to sort by. Defaults to {@link Direction#ASC}.
*
* @return
*/
Direction direction() default Direction.ASC;
/**
* Wrapper annotation to allow declaring multiple {@link SortDefault} annotations on a method parameter.
*
* @since 1.6
* @author Oliver Gierke
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface SortDefaults {
/**
* The individual {@link SortDefault} declarations to be sorted by.
*
* @return
*/
SortDefault[] value();
}
}

406
src/main/java/org/springframework/data/web/SortHandlerMethodArgumentResolver.java

@ -0,0 +1,406 @@ @@ -0,0 +1,406 @@
/*
* Copyright 2013 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
*
* http://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.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.MethodParameter;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.domain.Sort.Order;
import org.springframework.data.web.SortDefault.SortDefaults;
import org.springframework.hateoas.mvc.UriComponentsContributor;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.util.UriComponentsBuilder;
/**
* {@link HandlerMethodArgumentResolver} to automatically create {@link Sort} instances from request parameters or
* {@link SortDefault} annotations.
*
* @since 1.6
* @author Oliver Gierke
*/
public class SortHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver, UriComponentsContributor {
private static final String DEFAULT_PARAMETER = "sort";
private static final String DEFAULT_PROPERTY_DELIMITER = ",";
private static final String DEFAULT_QUALIFIER_DELIMITER = "_";
private static final String SORT_DEFAULTS_NAME = SortDefaults.class.getSimpleName();
private static final String SORT_DEFAULT_NAME = SortDefault.class.getSimpleName();
private String sortParameter = DEFAULT_PARAMETER;
private String propertyDelimiter = DEFAULT_PROPERTY_DELIMITER;
private String qualifierDelimiter = DEFAULT_QUALIFIER_DELIMITER;
boolean legacyMode = false;
/**
* Enables legacy mode parsing of the sorting parameter from the incoming request. Uses the sort property configured
* to lookup the fields to sort on and {@code $sortParameter.dir} for the direction.
*
* @param legacyMode whether to enable the legacy mode or not.
*/
@Deprecated
void setLegacyMode(boolean legacyMode) {
this.legacyMode = legacyMode;
}
/**
* Configure the request parameter to lookup sort information from. Defaults to {@code sort}.
*
* @param sortParameter must not be {@literal null} or empty.
*/
public void setSortParameter(String parameter) {
Assert.hasText(parameter);
this.sortParameter = parameter;
}
/**
* Configures the delimiter used to separate property references and the direction to be sorted by. Defaults to
* {@code}, which means sort values look like this: {@code firstname,lastname,asc}.
*
* @param propertyDelimiter must not be {@literal null} or empty.
*/
public void setPropertyDelimiter(String propertyDelimiter) {
Assert.hasText(propertyDelimiter, "Property delimiter must not be null or empty!");
this.propertyDelimiter = propertyDelimiter;
}
/**
* Configures the delimiter used to separate the qualifier from the sort parameter. Defaults to {@code _}, so a
* qualified sort property would look like {@code qualifier_sort}.
*
* @param qualifierDelimiter the qualifier delimiter to be used or {@literal null} to reset to the default.
*/
public void setQualifierDelimiter(String qualifierDelimiter) {
this.qualifierDelimiter = qualifierDelimiter == null ? DEFAULT_QUALIFIER_DELIMITER : qualifierDelimiter;
}
/*
* (non-Javadoc)
* @see org.springframework.web.method.support.HandlerMethodArgumentResolver#supportsParameter(org.springframework.core.MethodParameter)
*/
public boolean supportsParameter(MethodParameter parameter) {
return Sort.class.equals(parameter.getParameterType());
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.mvc.UriComponentsContributor#enhance(org.springframework.web.util.UriComponentsBuilder, org.springframework.core.MethodParameter, java.lang.Object)
*/
public void enhance(UriComponentsBuilder builder, MethodParameter parameter, Object value) {
if (!(value instanceof Sort)) {
return;
}
Sort sort = (Sort) value;
if (legacyMode) {
List<String> expressions = legacyFoldExpressions(sort);
Assert.isTrue(expressions.size() == 2,
String.format("Expected 2 sort expressions (fields, direction) but got %d!", expressions.size()));
builder.queryParam(getSortParameter(parameter), expressions.get(0));
builder.queryParam(getLegacyDirectionParameter(parameter), expressions.get(1));
} else {
for (String expression : foldIntoExpressions(sort)) {
builder.queryParam(getSortParameter(parameter), expression);
}
}
}
/*
* (non-Javadoc)
* @see org.springframework.web.method.support.HandlerMethodArgumentResolver#resolveArgument(org.springframework.core.MethodParameter, org.springframework.web.method.support.ModelAndViewContainer, org.springframework.web.context.request.NativeWebRequest, org.springframework.web.bind.support.WebDataBinderFactory)
*/
public Sort resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
String[] directionParameter = webRequest.getParameterValues(getSortParameter(parameter));
if (directionParameter != null && directionParameter.length != 0) {
return legacyMode ? parseLegacyParameterIntoSort(webRequest, parameter) : parseParameterIntoSort(
directionParameter, propertyDelimiter);
} else {
return getDefaults(parameter);
}
}
/**
* Reads the default {@link Sort} to be used from the given {@link MethodParameter}. Rejects the parameter if both an
* {@link SortDefaults} and {@link SortDefault} annotation is found as we cannot build a reliable {@link Sort}
* instance then (property ordering).
*
* @param parameter will never be {@literal null}.
* @return the default {@link Sort} instance derived from the parameter annotations or {@literal null}.
*/
private Sort getDefaults(MethodParameter parameter) {
SortDefaults annotatedDefaults = parameter.getParameterAnnotation(SortDefaults.class);
Sort sort = null;
if (annotatedDefaults != null) {
for (SortDefault annotatedDefault : annotatedDefaults.value()) {
sort = appendOrCreateSortTo(annotatedDefault, sort);
}
}
SortDefault annotatedDefault = parameter.getParameterAnnotation(SortDefault.class);
if (annotatedDefault == null) {
return sort;
}
if (sort != null && annotatedDefault != null) {
throw new IllegalArgumentException(String.format(
"Cannot use both @%s and @%s on parameter %s! Move %s into %s to define sorting order!", SORT_DEFAULTS_NAME,
SORT_DEFAULT_NAME, parameter.toString(), SORT_DEFAULT_NAME, SORT_DEFAULTS_NAME));
}
return appendOrCreateSortTo(annotatedDefault, sort);
}
/**
* Creates a new {@link Sort} instance from the given {@link SortDefault} or appends it to the given {@link Sort}
* instance if it's not {@literal null}.
*
* @param sortDefault
* @param sortOrNull
* @return
*/
private Sort appendOrCreateSortTo(SortDefault sortDefault, Sort sortOrNull) {
String[] fields = SpringDataAnnotationUtils.getSpecificPropertyOrDefaultFromValue(sortDefault, "sort");
if (fields.length == 0) {
return null;
}
Sort sort = new Sort(sortDefault.direction(), fields);
return sortOrNull == null ? sort : sortOrNull.and(sort);
}
/**
* Returns the sort parameter to be looked up from the request. Potentially applies qualifiers to it.
*
* @param parameter will never be {@literal null}.
* @return
*/
private String getSortParameter(MethodParameter parameter) {
StringBuilder builder = new StringBuilder();
if (parameter.hasParameterAnnotation(Qualifier.class)) {
builder.append(parameter.getParameterAnnotation(Qualifier.class).value()).append(qualifierDelimiter);
}
return builder.append(sortParameter).toString();
}
/**
* Creates a {@link Sort} instance from the given request expecting the {@link Direction} being encoded in a parameter
* with an appended {@code .dir}.
*
* @param request must not be {@literal null}.
* @param parameter must not be {@literal null}.
* @return
*/
private Sort parseLegacyParameterIntoSort(WebRequest request, MethodParameter parameter) {
String property = getSortParameter(parameter);
String fields = request.getParameter(property);
String directions = request.getParameter(getLegacyDirectionParameter(parameter));
return new Sort(Direction.fromStringOrNull(directions), fields.split(","));
}
private String getLegacyDirectionParameter(MethodParameter parameter) {
return getSortParameter(parameter) + ".dir";
}
/**
* Parses the given sort expressions into a {@link Sort} instance. The implementation expects the sources to be a
* concatenation of Strings using the given delimiter. If the last element can be parsed into a {@link Direction} it's
* considered a {@link Direction} and a simple property otherwise.
*
* @param source will never be {@literal null}.
* @param delimiter the delimiter to be used to split up the source elements, will never be {@literal null}.
* @return
*/
Sort parseParameterIntoSort(String[] source, String delimiter) {
List<Order> allOrders = new ArrayList<Sort.Order>();
for (String part : source) {
if (part == null) {
continue;
}
String[] elements = part.split(delimiter);
Direction direction = Direction.fromStringOrNull(elements[elements.length - 1]);
for (int i = 0; i < elements.length; i++) {
if (i == elements.length - 1 && direction != null) {
continue;
}
allOrders.add(new Order(direction, elements[i]));
}
}
return allOrders.isEmpty() ? null : new Sort(allOrders);
}
/**
* Folds the given {@link Sort} instance into a {@link List} of sort expressions, accumulating {@link Order} instances
* of the same direction into a single expression if they are in order.
*
* @param sort must not be {@literal null}.
* @return
*/
private List<String> foldIntoExpressions(Sort sort) {
List<String> expressions = new ArrayList<String>();
ExpressionBuilder builder = null;
for (Order order : sort) {
Direction direction = order.getDirection();
if (builder == null) {
builder = new ExpressionBuilder(direction);
} else if (!builder.hasSameDirectionAs(order)) {
builder.dumpExpressionIfPresentInto(expressions);
builder = new ExpressionBuilder(direction);
}
builder.add(order.getProperty());
}
return builder.dumpExpressionIfPresentInto(expressions);
}
/**
* Folds the given {@link Sort} instance into two expressions. The first being the property list, the second being the
* direction.
*
* @throws IllegalArgumentException if a {@link Sort} with multiple {@link Direction}s has been handed in.
* @param sort must not be {@literal null}.
* @return
*/
private List<String> legacyFoldExpressions(Sort sort) {
List<String> expressions = new ArrayList<String>();
ExpressionBuilder builder = null;
for (Order order : sort) {
Direction direction = order.getDirection();
if (builder == null) {
builder = new ExpressionBuilder(direction);
} else if (!builder.hasSameDirectionAs(order)) {
throw new IllegalArgumentException(String.format(
"%s in legacy configuration only supports a single direction to sort by!", getClass().getSimpleName()));
}
builder.add(order.getProperty());
}
return builder.dumpExpressionIfPresentInto(expressions);
}
/**
* Helper to easily build request parameter expressions for {@link Sort} instances.
*
* @author Oliver Gierke
*/
class ExpressionBuilder {
private final List<String> elements = new ArrayList<String>();
private final Direction direction;
/**
* Sets up a new {@link ExpressionBuilder} for properties to be sorted in the given {@link Direction}.
*
* @param direction must not be {@literal null}.
*/
public ExpressionBuilder(Direction direction) {
Assert.notNull(direction, "Direction must not be null!");
this.direction = direction;
}
/**
* Returns whether the given {@link Order} has the same direction as the current {@link ExpressionBuilder}.
*
* @param order must not be {@literal null}.
* @return
*/
public boolean hasSameDirectionAs(Order order) {
return this.direction == order.getDirection();
}
/**
* Adds the given property to the expression to be built.
*
* @param property
*/
public void add(String property) {
this.elements.add(property);
}
/**
* Dumps the expression currently in build into the given {@link List} of {@link String}s. Will only dump it in case
* there are properties piled up currently.
*
* @param expressions
* @return
*/
public List<String> dumpExpressionIfPresentInto(List<String> expressions) {
if (elements.isEmpty()) {
return expressions;
}
if (legacyMode) {
expressions.add(StringUtils.collectionToDelimitedString(elements, propertyDelimiter));
expressions.add(direction.name().toLowerCase());
} else {
elements.add(direction.name().toLowerCase());
expressions.add(StringUtils.collectionToDelimitedString(elements, propertyDelimiter));
}
return expressions;
}
}
}

142
src/main/java/org/springframework/data/web/SpringDataAnnotationUtils.java

@ -0,0 +1,142 @@ @@ -0,0 +1,142 @@
/*
* Copyright 2013 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
*
* http://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.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.Set;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.domain.Pageable;
import org.springframework.util.ObjectUtils;
/**
* Helper class to ease sharing code between legacy {@link PageableArgumentResolver} and
* {@link PageableHandlerMethodArgumentResolver}.
*
* @author Oliver Gierke
*/
class SpringDataAnnotationUtils {
/**
* Asserts uniqueness of all {@link Pageable} parameters of the method of the given {@link MethodParameter}.
*
* @param parameter must not be {@literal null}.
*/
public static void assertPageableUniqueness(MethodParameter parameter) {
Method method = parameter.getMethod();
if (containsMoreThanOnePageableParameter(method)) {
Annotation[][] annotations = method.getParameterAnnotations();
assertQualifiersFor(method.getParameterTypes(), annotations);
}
}
/**
* Returns whether the given {@link Method} has more than one {@link Pageable} parameter.
*
* @param method must not be {@literal null}.
* @return
*/
private static boolean containsMoreThanOnePageableParameter(Method method) {
boolean pageableFound = false;
for (Class<?> type : method.getParameterTypes()) {
if (pageableFound && type.equals(Pageable.class)) {
return true;
}
if (type.equals(Pageable.class)) {
pageableFound = true;
}
}
return false;
}
/**
* Returns the value of the given specific property of the given annotation. If the value of that property is the
* properties default, we fall back to the value of the {@code value} attribute.
*
* @param annotation must not be {@literal null}.
* @param property must not be {@literal null} or empty.
* @return
*/
@SuppressWarnings("unchecked")
public static <T> T getSpecificPropertyOrDefaultFromValue(Annotation annotation, String property) {
Object propertyDefaultValue = AnnotationUtils.getDefaultValue(annotation, property);
Object propertyValue = AnnotationUtils.getValue(annotation, property);
return (T) (ObjectUtils.nullSafeEquals(propertyDefaultValue, propertyValue) ? AnnotationUtils.getValue(annotation)
: propertyValue);
}
/**
* Asserts that every {@link Pageable} parameter of the given parameters carries an {@link Qualifier} annotation to
* distinguish them from each other.
*
* @param parameterTypes must not be {@literal null}.
* @param annotations must not be {@literal null}.
*/
public static void assertQualifiersFor(Class<?>[] parameterTypes, Annotation[][] annotations) {
Set<String> values = new HashSet<String>();
for (int i = 0; i < annotations.length; i++) {
if (Pageable.class.equals(parameterTypes[i])) {
Qualifier qualifier = findAnnotation(annotations[i]);
if (null == qualifier) {
throw new IllegalStateException(
"Ambiguous Pageable arguments in handler method. If you use multiple parameters of type Pageable you need to qualify them with @Qualifier");
}
if (values.contains(qualifier.value())) {
throw new IllegalStateException("Values of the user Qualifiers must be unique!");
}
values.add(qualifier.value());
}
}
}
/**
* Returns a {@link Qualifier} annotation from the given array of {@link Annotation}s. Returns {@literal null} if the
* array does not contain a {@link Qualifier} annotation.
*
* @param annotations must not be {@literal null}.
* @return
*/
public static Qualifier findAnnotation(Annotation[] annotations) {
for (Annotation annotation : annotations) {
if (annotation instanceof Qualifier) {
return (Qualifier) annotation;
}
}
return null;
}
}

113
src/test/java/org/springframework/data/web/PageableHandlerArgumentResolverUnitTests.java → src/test/java/org/springframework/data/web/LegacyPageableHandlerArgumentResolverUnitTests.java

@ -17,6 +17,7 @@ package org.springframework.data.web; @@ -17,6 +17,7 @@ package org.springframework.data.web;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import static org.springframework.data.web.PageableHandlerMethodArgumentResolver.*;
import java.lang.reflect.Method;
@ -27,19 +28,22 @@ import org.springframework.core.MethodParameter; @@ -27,19 +28,22 @@ import org.springframework.core.MethodParameter;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.web.SortDefault.SortDefaults;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.ServletWebRequest;
/**
* Unit tests for {@link PageableHandlerArgumentResolver}.
* Unit tests for {@link PageableHandlerMethodArgumentResolver} in it's legacy mode. Essentially a copy of
* {@link PageableArgumentResolverUnitTests} but but executed against {@link PageableHandlerMethodArgumentResolver}.
*
* @since 1.6
* @author Oliver Gierke
*/
public class PageableHandlerArgumentResolverUnitTests {
@SuppressWarnings("deprecation")
public class LegacyPageableHandlerArgumentResolverUnitTests extends PageableDefaultUnitTest {
Method correctMethod, failedMethod, invalidQualifiers, defaultsMethod, defaultsMethodWithSort,
Method correctMethod, noQualifiers, invalidQualifiers, defaultsMethod, defaultsMethodWithSort,
defaultsMethodWithSortAndDirection, otherMethod;
MockHttpServletRequest request;
@ -48,13 +52,13 @@ public class PageableHandlerArgumentResolverUnitTests { @@ -48,13 +52,13 @@ public class PageableHandlerArgumentResolverUnitTests {
public void setUp() throws SecurityException, NoSuchMethodException {
correctMethod = SampleController.class.getMethod("correctMethod", Pageable.class, Pageable.class);
failedMethod = SampleController.class.getMethod("failedMethod", Pageable.class, Pageable.class);
noQualifiers = SampleController.class.getMethod("noQualifiers", Pageable.class, Pageable.class);
invalidQualifiers = SampleController.class.getMethod("invalidQualifiers", Pageable.class, Pageable.class);
otherMethod = SampleController.class.getMethod("otherMethod", String.class);
otherMethod = SampleController.class.getMethod("unsupportedMethod", String.class);
defaultsMethod = SampleController.class.getMethod("defaultsMethod", Pageable.class);
defaultsMethodWithSort = SampleController.class.getMethod("defaultsMethodWithSort", Pageable.class);
defaultsMethodWithSortAndDirection = SampleController.class.getMethod("defaultsMethodWithSortAndDirection",
defaultsMethod = SampleController.class.getMethod("simpleDefault", Pageable.class);
defaultsMethodWithSort = SampleController.class.getMethod("simpleDefaultWithSort", Pageable.class);
defaultsMethodWithSortAndDirection = SampleController.class.getMethod("simpleDefaultWithSortAndDirection",
Pageable.class);
request = new MockHttpServletRequest();
@ -71,14 +75,14 @@ public class PageableHandlerArgumentResolverUnitTests { @@ -71,14 +75,14 @@ public class PageableHandlerArgumentResolverUnitTests {
@Test
public void supportsPageableParameter() {
PageableHandlerArgumentResolver resolver = new PageableHandlerArgumentResolver();
PageableHandlerMethodArgumentResolver resolver = new PageableHandlerMethodArgumentResolver();
resolver.supportsParameter(new MethodParameter(correctMethod, 0));
}
@Test
public void doesNotSupportNonPageableParameter() {
PageableHandlerArgumentResolver resolver = new PageableHandlerArgumentResolver();
PageableHandlerMethodArgumentResolver resolver = new PageableHandlerMethodArgumentResolver();
resolver.supportsParameter(new MethodParameter(otherMethod, 0));
}
@ -92,10 +96,10 @@ public class PageableHandlerArgumentResolverUnitTests { @@ -92,10 +96,10 @@ public class PageableHandlerArgumentResolverUnitTests {
@Test(expected = IllegalStateException.class)
public void rejectsInvalidlyMappedPageables() throws Exception {
MethodParameter parameter = new MethodParameter(failedMethod, 0);
MethodParameter parameter = new MethodParameter(noQualifiers, 0);
NativeWebRequest webRequest = new ServletWebRequest(request);
new PageableHandlerArgumentResolver().resolveArgument(parameter, null, webRequest, null);
new PageableHandlerMethodArgumentResolver().resolveArgument(parameter, null, webRequest, null);
}
@Test(expected = IllegalStateException.class)
@ -104,7 +108,7 @@ public class PageableHandlerArgumentResolverUnitTests { @@ -104,7 +108,7 @@ public class PageableHandlerArgumentResolverUnitTests {
MethodParameter parameter = new MethodParameter(invalidQualifiers, 0);
NativeWebRequest webRequest = new ServletWebRequest(request);
new PageableHandlerArgumentResolver().resolveArgument(parameter, null, webRequest, null);
new PageableHandlerMethodArgumentResolver().resolveArgument(parameter, null, webRequest, null);
}
@Test
@ -115,8 +119,8 @@ public class PageableHandlerArgumentResolverUnitTests { @@ -115,8 +119,8 @@ public class PageableHandlerArgumentResolverUnitTests {
assertThat(argument, is(instanceOf(Pageable.class)));
Pageable pageable = (Pageable) argument;
assertThat(pageable.getPageSize(), is(SampleController.DEFAULT_PAGESIZE));
assertThat(pageable.getPageNumber(), is(SampleController.DEFAULT_PAGENUMBER));
assertThat(pageable.getPageSize(), is(PAGE_SIZE));
assertThat(pageable.getPageNumber(), is(PAGE_NUMBER));
assertThat(pageable.getSort(), is(nullValue()));
}
@ -130,12 +134,12 @@ public class PageableHandlerArgumentResolverUnitTests { @@ -130,12 +134,12 @@ public class PageableHandlerArgumentResolverUnitTests {
mockRequest.addParameter("page.page", sizeParam.toString());
NativeWebRequest webRequest = new ServletWebRequest(mockRequest);
Object argument = new PageableHandlerArgumentResolver().resolveArgument(parameter, null, webRequest, null);
Object argument = LEGACY.resolveArgument(parameter, null, webRequest, null);
assertTrue(argument instanceof Pageable);
Pageable pageable = (Pageable) argument;
assertEquals(SampleController.DEFAULT_PAGESIZE, pageable.getPageSize());
assertEquals(PAGE_SIZE, pageable.getPageSize());
assertEquals(sizeParam - 1, pageable.getPageNumber());
}
@ -147,9 +151,9 @@ public class PageableHandlerArgumentResolverUnitTests { @@ -147,9 +151,9 @@ public class PageableHandlerArgumentResolverUnitTests {
assertThat(argument, is(instanceOf(Pageable.class)));
Pageable pageable = (Pageable) argument;
assertThat(pageable.getPageSize(), is(SampleController.DEFAULT_PAGESIZE));
assertThat(pageable.getPageNumber(), is(SampleController.DEFAULT_PAGENUMBER));
assertThat(pageable.getSort(), is(new Sort("foo")));
assertThat(pageable.getPageSize(), is(PAGE_SIZE));
assertThat(pageable.getPageNumber(), is(PAGE_NUMBER));
assertThat(pageable.getSort(), is(new Sort("firstname", "lastname")));
}
@Test
@ -160,9 +164,20 @@ public class PageableHandlerArgumentResolverUnitTests { @@ -160,9 +164,20 @@ public class PageableHandlerArgumentResolverUnitTests {
assertThat(argument, is(instanceOf(Pageable.class)));
Pageable pageable = (Pageable) argument;
assertThat(pageable.getPageSize(), is(SampleController.DEFAULT_PAGESIZE));
assertThat(pageable.getPageNumber(), is(SampleController.DEFAULT_PAGENUMBER));
assertThat(pageable.getSort(), is(new Sort(Direction.DESC, "foo")));
assertThat(pageable.getPageSize(), is(PAGE_SIZE));
assertThat(pageable.getPageNumber(), is(PAGE_NUMBER));
assertThat(pageable.getSort(), is(new Sort(Direction.DESC, "firstname", "lastname")));
}
@Test
public void buildsUpRequestParameters() {
// Set up basic page representation based on 1-indexed page numbers
String basicString = String.format("page.page=%d&page.size=%d", PAGE_NUMBER + 1, PAGE_SIZE);
assertUriStringFor(REFERENCE_WITHOUT_SORT, basicString);
assertUriStringFor(REFERENCE_WITH_SORT, basicString + "&page.sort=firstname,lastname&page.sort.dir=desc");
assertUriStringFor(REFERENCE_WITH_SORT_FIELDS, basicString + "&page.sort=firstname,lastname&page.sort.dir=asc");
}
private void assertSizeForPrefix(int size, Sort sort, int index) throws Exception {
@ -170,7 +185,7 @@ public class PageableHandlerArgumentResolverUnitTests { @@ -170,7 +185,7 @@ public class PageableHandlerArgumentResolverUnitTests {
MethodParameter parameter = new MethodParameter(correctMethod, index);
NativeWebRequest webRequest = new ServletWebRequest(request);
Object argument = new PageableHandlerArgumentResolver().resolveArgument(parameter, null, webRequest, null);
Object argument = LEGACY.resolveArgument(parameter, null, webRequest, null);
assertThat(argument, is(instanceOf(Pageable.class)));
Pageable pageable = (Pageable) argument;
@ -185,43 +200,45 @@ public class PageableHandlerArgumentResolverUnitTests { @@ -185,43 +200,45 @@ public class PageableHandlerArgumentResolverUnitTests {
MethodParameter parameter = new MethodParameter(method, 0);
NativeWebRequest webRequest = new ServletWebRequest(new MockHttpServletRequest());
return new PageableHandlerArgumentResolver().resolveArgument(parameter, null, webRequest, null);
}
static class SampleController {
static final int DEFAULT_PAGESIZE = 198;
static final int DEFAULT_PAGENUMBER = 42;
public void defaultsMethod(
@PageableDefaults(value = DEFAULT_PAGESIZE, pageNumber = DEFAULT_PAGENUMBER) Pageable pageable) {
return LEGACY.resolveArgument(parameter, null, webRequest, null);
}
}
@Override
protected Class<?> getControllerClass() {
return SampleController.class;
}
public void defaultsMethodWithSort(
@PageableDefaults(value = DEFAULT_PAGESIZE, pageNumber = DEFAULT_PAGENUMBER, sort = "foo") Pageable pageable) {
@Override
protected PageableHandlerMethodArgumentResolver getResolver() {
return PageableHandlerMethodArgumentResolver.LEGACY;
}
}
static interface SampleController {
public void defaultsMethodWithSortAndDirection(
@PageableDefaults(value = DEFAULT_PAGESIZE, pageNumber = DEFAULT_PAGENUMBER, sort = "foo", sortDir = Direction.DESC) Pageable pageable) {
void simpleDefault(@PageableDefaults(value = PAGE_SIZE, pageNumber = PAGE_NUMBER) Pageable pageable);
}
void simpleDefaultWithSort(@PageableDefaults(value = PAGE_SIZE, pageNumber = PAGE_NUMBER, sort = { "firstname",
"lastname" }) Pageable pageable);
public void correctMethod(@Qualifier("foo") Pageable first, @Qualifier("bar") Pageable second) {
void simpleDefaultWithSortAndDirection(@PageableDefaults(value = PAGE_SIZE, pageNumber = PAGE_NUMBER, sort = {
"firstname", "lastname" }, sortDir = Direction.DESC) Pageable pageable);
}
void simpleDefaultWithExternalSort(
@PageableDefaults(value = PAGE_SIZE, pageNumber = PAGE_NUMBER) @SortDefault(sort = { "firstname", "lastname" }, direction = Direction.DESC) Pageable pageable);
public void failedMethod(Pageable first, Pageable second) {
void simpleDefaultWithContaineredExternalSort(
@PageableDefaults(value = PAGE_SIZE, pageNumber = PAGE_NUMBER) @SortDefaults(@SortDefault(sort = { "firstname",
"lastname" }, direction = Direction.DESC)) Pageable pageable);
}
void correctMethod(@Qualifier("foo") Pageable first, @Qualifier("bar") Pageable second);
public void invalidQualifiers(@Qualifier("foo") Pageable first, @Qualifier("foo") Pageable second) {
void invalidQualifiers(@Qualifier("foo") Pageable first, @Qualifier("foo") Pageable second);
}
void noQualifiers(Pageable first, Pageable second);
public void otherMethod(String foo) {
void supportedMethod(Pageable pageable);
}
void unsupportedMethod(String foo);
}
}

5
src/test/java/org/springframework/data/web/PageableArgumentResolverUnitTests.java

@ -36,6 +36,7 @@ import org.springframework.web.context.request.ServletWebRequest; @@ -36,6 +36,7 @@ import org.springframework.web.context.request.ServletWebRequest;
*
* @author Oliver Gierke
*/
@SuppressWarnings("deprecation")
public class PageableArgumentResolverUnitTests {
Method correctMethod, failedMethod, invalidQualifiers, defaultsMethod, defaultsMethodWithSort,
@ -47,7 +48,7 @@ public class PageableArgumentResolverUnitTests { @@ -47,7 +48,7 @@ public class PageableArgumentResolverUnitTests {
public void setUp() throws SecurityException, NoSuchMethodException {
correctMethod = SampleController.class.getMethod("correctMethod", Pageable.class, Pageable.class);
failedMethod = SampleController.class.getMethod("failedMethod", Pageable.class, Pageable.class);
failedMethod = SampleController.class.getMethod("noQualifiers", Pageable.class, Pageable.class);
invalidQualifiers = SampleController.class.getMethod("invalidQualifiers", Pageable.class, Pageable.class);
defaultsMethod = SampleController.class.getMethod("defaultsMethod", Pageable.class);
@ -206,7 +207,7 @@ public class PageableArgumentResolverUnitTests { @@ -206,7 +207,7 @@ public class PageableArgumentResolverUnitTests {
}
public void failedMethod(Pageable first, Pageable second) {
public void noQualifiers(Pageable first, Pageable second) {
}

155
src/test/java/org/springframework/data/web/PageableDefaultUnitTest.java

@ -0,0 +1,155 @@ @@ -0,0 +1,155 @@
/*
* Copyright 2013 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
*
* http://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.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import static org.springframework.data.web.SortDefaultUnitTest.*;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.core.MethodParameter;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.util.UriComponentsBuilder;
/**
* Base test class to test supporting of a {@link HandlerMethodArgumentResolver} implementation defaulting
* {@link Pageable} method parameters. Expects the {@link HandlerMethodArgumentResolver} to be tested returned from
* {@link #getResolver()} and expects methods to be present in the controller class returned from
* {@link #getControllerClass()}. For sample usage see {@link PageableHandlerMethodArgumentResolver}.
*
* @since 1.6
* @author Oliver Gierke
*/
public abstract class PageableDefaultUnitTest {
static final int PAGE_SIZE = 47;
static final int PAGE_NUMBER = 23;
static final PageRequest REFERENCE_WITHOUT_SORT = new PageRequest(PAGE_NUMBER, PAGE_SIZE);
static final PageRequest REFERENCE_WITH_SORT = new PageRequest(PAGE_NUMBER, PAGE_SIZE, SORT);
static final PageRequest REFERENCE_WITH_SORT_FIELDS = new PageRequest(PAGE_NUMBER, PAGE_SIZE, new Sort(SORT_FIELDS));
@Rule
public ExpectedException exception = ExpectedException.none();
@Test
public void supportsPageable() {
assertThat(getResolver().supportsParameter(getParameterOfMethod("supportedMethod")), is(true));
}
@Test
public void doesNotSupportNonPageable() {
MethodParameter parameter = TestUtils.getParameterOfMethod(getControllerClass(), "unsupportedMethod", String.class);
assertThat(getResolver().supportsParameter(parameter), is(false));
}
@Test
public void returnsDefaultIfNoRequestParametersAndNoDefault() throws Exception {
assertSupportedAndResult(getParameterOfMethod("supportedMethod"),
(Pageable) ReflectionTestUtils.getField(getResolver(), "fallbackPageable"));
}
@Test
public void simpleDefault() throws Exception {
assertSupportedAndResult(getParameterOfMethod("simpleDefault"), REFERENCE_WITHOUT_SORT);
}
@Test
public void simpleDefaultWithSort() throws Exception {
assertSupportedAndResult(getParameterOfMethod("simpleDefaultWithSort"), REFERENCE_WITH_SORT_FIELDS);
}
@Test
public void simpleDefaultWithSortAndDirection() throws Exception {
assertSupportedAndResult(getParameterOfMethod("simpleDefaultWithSortAndDirection"), REFERENCE_WITH_SORT);
}
@Test
public void simpleDefaultWithExternalSort() throws Exception {
assertSupportedAndResult(getParameterOfMethod("simpleDefaultWithExternalSort"), REFERENCE_WITH_SORT);
}
@Test
public void simpleDefaultWithContaineredExternalSort() throws Exception {
assertSupportedAndResult(getParameterOfMethod("simpleDefaultWithContaineredExternalSort"), REFERENCE_WITH_SORT);
}
@Test
public void rejectsInvalidQulifiers() throws Exception {
MethodParameter parameter = TestUtils.getParameterOfMethod(getControllerClass(), "invalidQualifiers",
Pageable.class, Pageable.class);
HandlerMethodArgumentResolver resolver = getResolver();
assertThat(resolver.supportsParameter(parameter), is(true));
exception.expect(IllegalStateException.class);
exception.expectMessage("unique");
resolver.resolveArgument(parameter, null, TestUtils.getWebRequest(), null);
}
@Test
public void rejectsNoQualifiers() throws Exception {
MethodParameter parameter = TestUtils.getParameterOfMethod(getControllerClass(), "noQualifiers", Pageable.class,
Pageable.class);
HandlerMethodArgumentResolver resolver = getResolver();
assertThat(resolver.supportsParameter(parameter), is(true));
exception.expect(IllegalStateException.class);
exception.expectMessage("Ambiguous");
resolver.resolveArgument(parameter, null, TestUtils.getWebRequest(), null);
}
private void assertSupportedAndResult(MethodParameter parameter, Pageable pageable) throws Exception {
HandlerMethodArgumentResolver resolver = getResolver();
assertThat(resolver.supportsParameter(parameter), is(true));
assertThat(resolver.resolveArgument(parameter, null, TestUtils.getWebRequest(), null), is((Object) pageable));
}
protected void assertUriStringFor(Pageable pageable, String expected) {
UriComponentsBuilder builder = UriComponentsBuilder.fromPath("/");
MethodParameter parameter = getParameterOfMethod("supportedMethod");
getResolver().enhance(builder, parameter, pageable);
assertThat(builder.build().toUriString(), endsWith(expected));
}
protected abstract PageableHandlerMethodArgumentResolver getResolver();
protected abstract Class<?> getControllerClass();
protected MethodParameter getParameterOfMethod(String name) {
return getParameterOfMethod(getControllerClass(), name);
}
private static MethodParameter getParameterOfMethod(Class<?> controller, String name) {
return TestUtils.getParameterOfMethod(controller, name, Pageable.class);
}
}

76
src/test/java/org/springframework/data/web/PageableHandlerMethodArgumentResolverUnitTest.java

@ -0,0 +1,76 @@ @@ -0,0 +1,76 @@
/*
* Copyright 2013 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.web;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.web.SortDefault.SortDefaults;
/**
* Unit tests for {@link PageableHandlerMethodArgumentResolver}. Pulls in defaulting tests from
* {@link PageableDefaultUnitTest}.
*
* @author Oliver Gierke
*/
public class PageableHandlerMethodArgumentResolverUnitTest extends PageableDefaultUnitTest {
@Test
public void buildsUpRequestParameters() {
String basicString = String.format("page=%d&size=%d", PAGE_NUMBER, PAGE_SIZE);
assertUriStringFor(REFERENCE_WITHOUT_SORT, basicString);
assertUriStringFor(REFERENCE_WITH_SORT, basicString + "&sort=firstname,lastname,desc");
assertUriStringFor(REFERENCE_WITH_SORT_FIELDS, basicString + "&sort=firstname,lastname,asc");
}
@Override
protected PageableHandlerMethodArgumentResolver getResolver() {
return new PageableHandlerMethodArgumentResolver();
}
@Override
protected Class<?> getControllerClass() {
return Sample.class;
}
interface Sample {
void supportedMethod(Pageable pageable);
void unsupportedMethod(String string);
void simpleDefault(@PageableDefault(size = PAGE_SIZE, page = PAGE_NUMBER) Pageable pageable);
void simpleDefaultWithSort(
@PageableDefault(size = PAGE_SIZE, page = PAGE_NUMBER, sort = { "firstname", "lastname" }) Pageable pageable);
void simpleDefaultWithSortAndDirection(@PageableDefault(size = PAGE_SIZE, page = PAGE_NUMBER, sort = { "firstname",
"lastname" }, direction = Direction.DESC) Pageable pageable);
void simpleDefaultWithExternalSort(@PageableDefault(size = PAGE_SIZE, page = PAGE_NUMBER)//
@SortDefault(sort = { "firstname", "lastname" }, direction = Direction.DESC) Pageable pageable);
void simpleDefaultWithContaineredExternalSort(@PageableDefault(size = PAGE_SIZE, page = PAGE_NUMBER)//
@SortDefaults(@SortDefault(sort = { "firstname", "lastname" }, direction = Direction.DESC)) Pageable pageable);
void invalidQualifiers(@Qualifier("foo") Pageable first, @Qualifier("foo") Pageable second);
void noQualifiers(Pageable first, Pageable second);
}
}

143
src/test/java/org/springframework/data/web/SortDefaultUnitTest.java

@ -0,0 +1,143 @@ @@ -0,0 +1,143 @@
/*
* Copyright 2013 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
*
* http://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.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import static org.springframework.data.domain.Sort.Direction.*;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.core.MethodParameter;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.domain.Sort.Order;
import org.springframework.data.web.SortDefault.SortDefaults;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
/**
* Unit tests for {@link SortDefault}.
*
* @since 1.6
* @author Oliver Gierke
*/
public abstract class SortDefaultUnitTest {
static final String SORT_0 = "username";
static final String SORT_1 = "username,asc";
static final String[] SORT_2 = new String[] { "username,ASC", "lastname,firstname,DESC" };
static final String SORT_3 = "firstname,lastname";
static final String[] SORT_FIELDS = new String[] { "firstname", "lastname" };
static final Direction SORT_DIRECTION = Direction.DESC;
static final Sort SORT = new Sort(SORT_DIRECTION, SORT_FIELDS);
@Rule
public ExpectedException exception = ExpectedException.none();
@Test
public void parsesSimpleSortStringCorrectly() {
assertSortStringParsedInto(new Sort(new Order("username")), SORT_1);
assertSortStringParsedInto(new Sort(new Order(ASC, "username")), SORT_1);
assertSortStringParsedInto(new Sort(new Order(ASC, "username"), //
new Order(DESC, "lastname"), new Order(DESC, "firstname")), SORT_2);
assertSortStringParsedInto(new Sort("firstname", "lastname"), SORT_3);
}
private static void assertSortStringParsedInto(Sort expected, String... source) {
SortHandlerMethodArgumentResolver resolver = new SortHandlerMethodArgumentResolver();
Sort sort = resolver.parseParameterIntoSort(source, ",");
assertThat(sort, is(expected));
}
@Test
public void supportsSortParameter() {
assertThat(getResolver().supportsParameter(getParameterOfMethod("supportedMethod")), is(true));
}
@Test
public void returnsNullForNoDefault() throws Exception {
assertSupportedAndResolvedTo(getParameterOfMethod("supportedMethod"), null);
}
@Test
public void discoversSimpleDefault() throws Exception {
assertSupportedAndResolvedTo(getParameterOfMethod("simpleDefault"), new Sort(Direction.ASC, SORT_FIELDS));
}
@Test
public void discoversSimpleDefaultWithDirection() throws Exception {
assertSupportedAndResolvedTo(getParameterOfMethod("simpleDefaultWithDirection"), SORT);
}
@Test
public void rejectsNonSortParameter() {
MethodParameter parameter = TestUtils.getParameterOfMethod(getControllerClass(), "unsupportedMethod", String.class);
assertThat(getResolver().supportsParameter(parameter), is(false));
}
@Test
public void rejectsDoubleAnnotatedMethod() throws Exception {
MethodParameter parameter = getParameterOfMethod("invalid");
HandlerMethodArgumentResolver resolver = new SortHandlerMethodArgumentResolver();
assertThat(resolver.supportsParameter(parameter), is(true));
exception.expect(IllegalArgumentException.class);
exception.expectMessage(SortDefault.class.getSimpleName());
exception.expectMessage(SortDefaults.class.getSimpleName());
exception.expectMessage(parameter.toString());
resolver.resolveArgument(parameter, null, TestUtils.getWebRequest(), null);
}
@Test
public void discoversContaineredDefault() throws Exception {
MethodParameter parameter = getParameterOfMethod("containeredDefault");
Sort reference = new Sort("foo", "bar");
assertSupportedAndResolvedTo(parameter, reference);
}
protected HandlerMethodArgumentResolver getResolver() {
return new SortHandlerMethodArgumentResolver();
}
protected abstract Class<?> getControllerClass();
private void assertSupportedAndResolvedTo(MethodParameter parameter, Sort sort) throws Exception {
HandlerMethodArgumentResolver resolver = getResolver();
assertThat(resolver.supportsParameter(parameter), is(true));
assertThat(resolver.resolveArgument(parameter, null, TestUtils.getWebRequest(), null), is((Object) sort));
}
protected MethodParameter getParameterOfMethod(String name) {
return getParameterOfMethod(getControllerClass(), name);
}
private static MethodParameter getParameterOfMethod(Class<?> controller, String name) {
return TestUtils.getParameterOfMethod(controller, name, Sort.class);
}
}

164
src/test/java/org/springframework/data/web/SortHandlerArgumentResolverUnitTests.java

@ -0,0 +1,164 @@ @@ -0,0 +1,164 @@
/*
* Copyright 2013 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
*
* http://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.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import static org.springframework.data.domain.Sort.Direction.*;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.MethodParameter;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.domain.Sort.Order;
import org.springframework.data.web.SortDefault.SortDefaults;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.util.UriComponentsBuilder;
/**
* Unit tests for {@link SortHandlerMethodArgumentResolver}.
*
* @since 1.6
* @author Oliver Gierke
*/
public class SortHandlerArgumentResolverUnitTests extends SortDefaultUnitTest {
static final String SORT_0 = "username";
static final String SORT_1 = "username,asc";
static final String[] SORT_2 = new String[] { "username,ASC", "lastname,firstname,DESC" };
static final String SORT_3 = "firstname,lastname";
@Test
public void discoversSimpleSortFromRequest() {
MethodParameter parameter = getParameterOfMethod("simpleDefault");
Sort reference = new Sort("bar", "foo");
NativeWebRequest request = getRequestWithSort(reference);
assertSupportedAndResolvedTo(request, parameter, reference);
}
@Test
public void discoversComplexSortFromRequest() {
MethodParameter parameter = getParameterOfMethod("simpleDefault");
Sort reference = new Sort("bar", "foo").and(new Sort("fizz", "buzz"));
assertSupportedAndResolvedTo(getRequestWithSort(reference), parameter, reference);
}
@Test
public void discoversQualifiedSortFromRequest() {
MethodParameter parameter = getParameterOfMethod("qualifiedSort");
Sort reference = new Sort("bar", "foo");
assertSupportedAndResolvedTo(getRequestWithSort(reference, "qual"), parameter, reference);
}
@Test
public void returnsNullForSortParameterSetToNothing() throws Exception {
MethodParameter parameter = getParameterOfMethod("supportedMethod");
MockHttpServletRequest request = new MockHttpServletRequest();
request.addParameter("sort", (String) null);
SortHandlerMethodArgumentResolver resolver = new SortHandlerMethodArgumentResolver();
Sort result = resolver.resolveArgument(parameter, null, new ServletWebRequest(request), null);
assertThat(result, is(nullValue()));
}
@Test
public void buildsUpRequestParameters() {
assertUriStringFor(SORT, "sort=firstname,lastname,desc");
assertUriStringFor(new Sort(ASC, "foo").and(new Sort(DESC, "bar").and(new Sort(ASC, "foobar"))),
"sort=foo,asc&sort=bar,desc&sort=foobar,asc");
assertUriStringFor(new Sort(ASC, "foo").and(new Sort(ASC, "bar").and(new Sort(DESC, "foobar"))),
"sort=foo,bar,asc&sort=foobar,desc");
}
private void assertUriStringFor(Sort sort, String expected) {
UriComponentsBuilder builder = UriComponentsBuilder.fromPath("/");
MethodParameter parameter = getParameterOfMethod("supportedMethod");
new SortHandlerMethodArgumentResolver().enhance(builder, parameter, sort);
assertThat(builder.build().toUriString(), endsWith(expected));
}
private static void assertSupportedAndResolvedTo(NativeWebRequest request, MethodParameter parameter, Sort sort) {
SortHandlerMethodArgumentResolver resolver = new SortHandlerMethodArgumentResolver();
assertThat(resolver.supportsParameter(parameter), is(true));
try {
assertThat(resolver.resolveArgument(parameter, null, request, null), is(sort));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private static NativeWebRequest getRequestWithSort(Sort sort) {
return getRequestWithSort(sort, null);
}
private static NativeWebRequest getRequestWithSort(Sort sort, String qualifier) {
MockHttpServletRequest request = new MockHttpServletRequest();
if (sort == null) {
return new ServletWebRequest(request);
}
for (Order order : sort) {
String prefix = StringUtils.hasText(qualifier) ? qualifier + "_" : "";
request.addParameter(prefix + "sort", String.format("%s,%s", order.getProperty(), order.getDirection().name()));
}
return new ServletWebRequest(request);
}
@Override
protected Class<?> getControllerClass() {
return Controller.class;
}
interface Controller {
void supportedMethod(Sort sort);
void unsupportedMethod(String string);
void qualifiedSort(@Qualifier("qual") Sort sort);
void simpleDefault(@SortDefault({ "firstname", "lastname" }) Sort sort);
void simpleDefaultWithDirection(
@SortDefault(sort = { "firstname", "lastname" }, direction = Direction.DESC) Sort sort);
void containeredDefault(@SortDefaults(@SortDefault({ "foo", "bar" })) Sort sort);
void invalid(@SortDefaults(@SortDefault({ "foo", "bar" })) @SortDefault({ "bar", "foo" }) Sort sort);
}
}

51
src/test/java/org/springframework/data/web/TestUtils.java

@ -0,0 +1,51 @@ @@ -0,0 +1,51 @@
/*
* Copyright 2013 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
*
* http://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 org.springframework.core.MethodParameter;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.ServletWebRequest;
/**
* General test utilities.
*
* @since 1.6
* @author Oliver Gierke
*/
class TestUtils {
public static NativeWebRequest getWebRequest() {
return new ServletWebRequest(new MockHttpServletRequest());
}
public static MethodParameter getParameterOfMethod(Class<?> controller, String name, Class<?>... argumentTypes) {
Method method = getMethod(controller, name, argumentTypes);
return new MethodParameter(method, 0);
}
public static Method getMethod(Class<?> controller, String name, Class<?>... argumentTypes) {
try {
return controller.getMethod(name, argumentTypes);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
Loading…
Cancel
Save