From 6c90a9cfcb50b2ed0e7a25db0cfd64d36e7065da Mon Sep 17 00:00:00 2001 From: Muhammad Ichsan Date: Wed, 29 Jun 2016 00:41:47 +0800 Subject: [PATCH] DATACMNS-531 - Add support for a simplified syntax in request parameters for Sort --- ...mpleSortHandlerMethodArgumentResolver.java | 382 ++++++++++++++++++ .../data/web/SimpleSortDefaultUnitTests.java | 152 +++++++ ...andlerMethodArgumentResolverUnitTests.java | 280 +++++++++++++ 3 files changed, 814 insertions(+) create mode 100644 src/main/java/org/springframework/data/web/SimpleSortHandlerMethodArgumentResolver.java create mode 100644 src/test/java/org/springframework/data/web/SimpleSortDefaultUnitTests.java create mode 100644 src/test/java/org/springframework/data/web/SimpleSortHandlerMethodArgumentResolverUnitTests.java diff --git a/src/main/java/org/springframework/data/web/SimpleSortHandlerMethodArgumentResolver.java b/src/main/java/org/springframework/data/web/SimpleSortHandlerMethodArgumentResolver.java new file mode 100644 index 0000000000..bac2b008ff --- /dev/null +++ b/src/main/java/org/springframework/data/web/SimpleSortHandlerMethodArgumentResolver.java @@ -0,0 +1,382 @@ +/* + * Copyright 2013-2015 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.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.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 java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * {@link HandlerMethodArgumentResolver} to automatically create {@link Sort} instances from request parameters or + * {@link SortDefault} annotations. + * + * @since 1.6 + * @author Oliver Gierke + * @author Thomas Darimont + * @author Nick Williams + * @author Muhammad Ichsan + */ +public class SimpleSortHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver { + + 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 DEFAULT_ASCENDING_SIGN = ""; + private static final Sort DEFAULT_SORT = null; + + private static final String SORT_DEFAULTS_NAME = SortDefaults.class.getSimpleName(); + private static final String SORT_DEFAULT_NAME = SortDefault.class.getSimpleName(); + + private Sort fallbackSort = DEFAULT_SORT; + private String sortParameter = DEFAULT_PARAMETER; + private String propertyDelimiter = DEFAULT_PROPERTY_DELIMITER; + private String qualifierDelimiter = DEFAULT_QUALIFIER_DELIMITER; + private String ascendingSign = DEFAULT_ASCENDING_SIGN; + + /** + * Configures the sign used to mark ascending property. Defaults to {@code }, so a + * qualified sort property would look like {@code qualifier_sort}. + * + * @param ascendingSign must not be {@literal null} or empty. + */ + public void setAscendingSign(String ascendingSign) { + Assert.hasText(ascendingSign); + this.ascendingSign = ascendingSign; + } + + /** + * 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 sortParameter) { + + Assert.hasText(sortParameter); + this.sortParameter = sortParameter; + } + + /** + * 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 -birthDate,name}. + * + * @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) + */ + @Override + public boolean supportsParameter(MethodParameter parameter) { + return Sort.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) + */ + @Override + public Sort resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { + + String directionParameter = webRequest.getParameter(getSortParameter(parameter)); + + // No parameter + if (directionParameter == null) { + return getDefaultFromAnnotationOrFallback(parameter); + } + + // Single empty parameter, e.g "sort=" + if (!StringUtils.hasText(directionParameter)) { + return getDefaultFromAnnotationOrFallback(parameter); + } + + return parseParameterIntoSort(directionParameter, propertyDelimiter); + } + + /** + * 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 the configured fallback-sort + * {@link #setFallbackSort(Sort)}. + */ + private Sort getDefaultFromAnnotationOrFallback(MethodParameter parameter) { + + SortDefaults annotatedDefaults = parameter.getParameterAnnotation(SortDefaults.class); + SortDefault annotatedDefault = parameter.getParameterAnnotation(SortDefault.class); + + if (annotatedDefault != null && annotatedDefaults != 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)); + } + + if (annotatedDefault != null) { + return appendOrCreateSortTo(annotatedDefault, null); + } + + if (annotatedDefaults != null) { + Sort sort = null; + for (SortDefault currentAnnotatedDefault : annotatedDefaults.value()) { + sort = appendOrCreateSortTo(currentAnnotatedDefault, sort); + } + return sort; + } + + return fallbackSort; + } + + /** + * 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 + */ + protected String getSortParameter(MethodParameter parameter) { + + StringBuilder builder = new StringBuilder(); + + if (parameter != null && parameter.hasParameterAnnotation(Qualifier.class)) { + builder.append(parameter.getParameterAnnotation(Qualifier.class).value()).append(qualifierDelimiter); + } + + return builder.append(sortParameter).toString(); + } + + /** + * 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 part 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 part, String delimiter) { + + List allOrders = new ArrayList(); + + if (part != null) { + String[] elements = part.split(delimiter); + + for (int i = 0; i < elements.length; i++) { + String property = elements[i]; + + Direction direction = null; + if (property.startsWith("-")) { + property = property.substring(1, property.length()); + direction = Direction.DESC; + } else if (ascendingSign.isEmpty() || property.startsWith(ascendingSign)) { + property = property.substring(ascendingSign.length(), property.length()); + direction = Direction.ASC; + } + + if (!StringUtils.hasText(property)) { + continue; + } + + if (direction != null) { + allOrders.add(new Order(direction, property)); + } + } + } + + 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 + */ + protected List foldIntoExpressions(Sort sort) { + + List expressions = new ArrayList(); + 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 == null ? Collections. emptyList() : 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 + */ + protected List legacyFoldExpressions(Sort sort) { + + List expressions = new ArrayList(); + 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 == null ? Collections. emptyList() : builder.dumpExpressionIfPresentInto(expressions); + } + + /** + * Helper to easily build request parameter expressions for {@link Sort} instances. + * + * @author Oliver Gierke + */ + class ExpressionBuilder { + + private final List elements = new ArrayList(); + 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 dumpExpressionIfPresentInto(List expressions) { + + if (elements.isEmpty()) { + return expressions; + } + + elements.add(direction.name().toLowerCase()); + expressions.add(StringUtils.collectionToDelimitedString(elements, propertyDelimiter)); + + return expressions; + } + } + + /** + * Configures the {@link Sort} to be used as fallback in case no {@link SortDefault} or {@link SortDefaults} (the + * latter only supported in legacy mode) can be found at the method parameter to be resolved. + *

+ * If you set this to {@literal null}, be aware that you controller methods will get {@literal null} handed into them + * in case no {@link Sort} data can be found in the request. + * + * @param fallbackSort the {@link Sort} to be used as general fallback. + */ + public void setFallbackSort(Sort fallbackSort) { + this.fallbackSort = fallbackSort; + } +} diff --git a/src/test/java/org/springframework/data/web/SimpleSortDefaultUnitTests.java b/src/test/java/org/springframework/data/web/SimpleSortDefaultUnitTests.java new file mode 100644 index 0000000000..23c8a49308 --- /dev/null +++ b/src/test/java/org/springframework/data/web/SimpleSortDefaultUnitTests.java @@ -0,0 +1,152 @@ +/* + * 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.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; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.springframework.data.domain.Sort.Direction.ASC; +import static org.springframework.data.domain.Sort.Direction.DESC; + +/** + * Unit tests for {@link SortDefault}. + * + * @since 1.6 + * @author Oliver Gierke + * @author Muhammad Ichsan + */ +public abstract class SimpleSortDefaultUnitTests { + + static final String SORT_0 = "username"; + static final String SORT_1 = "firstname,lastname"; + static final String SORT_2 = "-created,title"; + static final String SORT_3 = "username,asc"; + + 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_0); + assertSortStringParsedInto(new Sort("firstname", "lastname"), SORT_1); + assertSortStringParsedInto(new Sort(new Order(DESC, "created"), new Order(ASC, "title")), SORT_2); + assertSortStringParsedInto(new Sort("username", "asc"), SORT_3); + } + + private static void assertSortStringParsedInto(Sort expected, String source) { + SimpleSortHandlerMethodArgumentResolver resolver = new SimpleSortHandlerMethodArgumentResolver(); + Sort sort = resolver.parseParameterIntoSort(source, ","); + + assertThat(sort, is(expected)); + } + + @Test + public void supportsCustomAscendingSign() { + SimpleSortHandlerMethodArgumentResolver resolver = new SimpleSortHandlerMethodArgumentResolver(); + resolver.setAscendingSign("+"); + Sort sort = resolver.parseParameterIntoSort("+name,-created,-modified,+subject", ","); + + assertThat(sort, is(new Sort(new Order("name"), new Order(DESC, "created"), + new Order(DESC, "modified"), new Order("subject")))); + } + + @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 SimpleSortHandlerMethodArgumentResolver(); + 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 SimpleSortHandlerMethodArgumentResolver(); + } + + 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); + } +} diff --git a/src/test/java/org/springframework/data/web/SimpleSortHandlerMethodArgumentResolverUnitTests.java b/src/test/java/org/springframework/data/web/SimpleSortHandlerMethodArgumentResolverUnitTests.java new file mode 100644 index 0000000000..8aab5bf1b0 --- /dev/null +++ b/src/test/java/org/springframework/data/web/SimpleSortHandlerMethodArgumentResolverUnitTests.java @@ -0,0 +1,280 @@ +/* + * Copyright 2013-2015 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.BeforeClass; +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 javax.servlet.http.HttpServletRequest; + +import java.util.ArrayList; +import java.util.List; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.junit.Assert.assertThat; +import static org.springframework.data.domain.Sort.Direction.DESC; + +/** + * Unit tests for {@link SimpleSortHandlerMethodArgumentResolver}. + * + * @since 1.6 + * @author Oliver Gierke + * @author Thomas Darimont + * @author Nick Williams + * @author Muhammad Ichsan + */ +public class SimpleSortHandlerMethodArgumentResolverUnitTests extends SimpleSortDefaultUnitTests { + + static MethodParameter PARAMETER; + + @BeforeClass + public static void setUp() throws Exception { + PARAMETER = new MethodParameter(Controller.class.getMethod("supportedMethod", Sort.class), 0); + } + + /** + * @see DATACMNS-351 + */ + @Test + public void fallbackToGivenDefaultSort() throws Exception { + + MethodParameter parameter = TestUtils.getParameterOfMethod(getControllerClass(), "unsupportedMethod", String.class); + SimpleSortHandlerMethodArgumentResolver resolver = new SimpleSortHandlerMethodArgumentResolver(); + Sort fallbackSort = new Sort(Direction.ASC, "ID"); + resolver.setFallbackSort(fallbackSort); + + Sort sort = resolver.resolveArgument(parameter, null, new ServletWebRequest(new MockHttpServletRequest()), null); + assertThat(sort, is(fallbackSort)); + } + + /** + * @see DATACMNS-351 + */ + @Test + public void fallbackToDefaultDefaultSort() throws Exception { + + MethodParameter parameter = TestUtils.getParameterOfMethod(getControllerClass(), "unsupportedMethod", String.class); + SimpleSortHandlerMethodArgumentResolver resolver = new SimpleSortHandlerMethodArgumentResolver(); + + Sort sort = resolver.resolveArgument(parameter, null, new ServletWebRequest(new MockHttpServletRequest()), null); + assertThat(sort, is(nullValue())); + } + + @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); + + SimpleSortHandlerMethodArgumentResolver resolver = new SimpleSortHandlerMethodArgumentResolver(); + Sort result = resolver.resolveArgument(parameter, null, new ServletWebRequest(request), null); + assertThat(result, is(nullValue())); + } + + /** + * @see DATACMNS-366 + * @throws Exception + */ + @Test + public void requestForMultipleSortPropertiesIsUnmarshalledCorrectly() throws Exception { + + MethodParameter parameter = getParameterOfMethod("supportedMethod"); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addParameter("sort", SORT_1); + + SimpleSortHandlerMethodArgumentResolver resolver = new SimpleSortHandlerMethodArgumentResolver(); + Sort result = resolver.resolveArgument(parameter, null, new ServletWebRequest(request), null); + assertThat(result, is(new Sort(Direction.ASC, "firstname", "lastname"))); + } + + /** + * @see DATACMNS-408 + */ + @Test + public void parsesEmptySortToNull() throws Exception { + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addParameter("sort", ""); + + assertThat(resolveSort(request, PARAMETER), is(nullValue())); + } + + /** + * @see DATACMNS-408 + */ + @Test + public void sortParamIsInvalidProperty() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addParameter("sort", "-"); + + assertThat(resolveSort(request, PARAMETER), is(nullValue())); + } + + /** + * @see DATACMNS-408 + */ + @Test + public void sortParamIsInvalidPropertyWhenMultiProperty() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addParameter("sort", "-property1,,"); + + assertThat(resolveSort(request, PARAMETER), is(new Sort(DESC, "property1"))); + } + + @Test + public void sortParamOnlyTakeTheFirstSort() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addParameter("sort", "-property"); + request.addParameter("sort", ""); + + assertThat(resolveSort(request, PARAMETER), is(new Sort(DESC, "property"))); + } + + /** + * @see DATACMNS-379 + */ + @Test + public void parsesCommaParameterForSort() throws Exception { + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addParameter("sort", ","); + + assertThat(resolveSort(request, PARAMETER), is(nullValue())); + } + + /** + * @see DATACMNS-753, DATACMNS-408 + */ + @Test + public void doesNotReturnNullWhenAnnotatedWithSortDefault() throws Exception { + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addParameter("sort", ""); + + assertThat(resolveSort(request, getParameterOfMethod("simpleDefault")), is(new Sort("firstname", "lastname"))); + assertThat(resolveSort(request, getParameterOfMethod("containeredDefault")), is(new Sort("foo", "bar"))); + } + + private static Sort resolveSort(HttpServletRequest request, MethodParameter parameter) throws Exception { + + SimpleSortHandlerMethodArgumentResolver resolver = new SimpleSortHandlerMethodArgumentResolver(); + return resolver.resolveArgument(parameter, null, new ServletWebRequest(request), null); + } + + private static void assertSupportedAndResolvedTo(NativeWebRequest request, MethodParameter parameter, Sort sort) { + + SimpleSortHandlerMethodArgumentResolver resolver = new SimpleSortHandlerMethodArgumentResolver(); + 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); + } + + String prefix = StringUtils.hasText(qualifier) ? qualifier + "_" : ""; + List construction = new ArrayList(); + for (Order order : sort) { + String sign = ""; + if (order.getDirection() == DESC) { + sign = "-"; + } + construction.add(String.format("%s%s", sign, order.getProperty())); + } + + request.addParameter(prefix + "sort", StringUtils.arrayToDelimitedString(construction.toArray(), ",")); + + 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); + } +}