Skip to content

Commit 9c53c59

Browse files
committed
Refactor SortStrategy
For a completely generic SortStrategy we can't provide any concrete support we know neither the input structure nor the Object structure. At best, a nearly empty argument resolver, which is not any better than providing your own custom argument resolver. Hence, SortStrategy is now explicitly based on Spring Data's Sort, which enables us to provide concrete support with a base class. See gh-620
1 parent a0af83d commit 9c53c59

File tree

8 files changed

+228
-43
lines changed

8 files changed

+228
-43
lines changed

spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/AnnotatedControllerConfigurer.java

Lines changed: 12 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848

4949
import org.springframework.aop.support.AopUtils;
5050
import org.springframework.beans.factory.InitializingBean;
51+
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
5152
import org.springframework.context.ApplicationContext;
5253
import org.springframework.context.ApplicationContextAware;
5354
import org.springframework.context.expression.BeanFactoryResolver;
@@ -57,7 +58,6 @@
5758
import org.springframework.core.ResolvableType;
5859
import org.springframework.core.annotation.AnnotatedElementUtils;
5960
import org.springframework.core.convert.ConversionService;
60-
import org.springframework.expression.BeanResolver;
6161
import org.springframework.format.FormatterRegistrar;
6262
import org.springframework.format.support.DefaultFormattingConversionService;
6363
import org.springframework.format.support.FormattingConversionService;
@@ -68,7 +68,7 @@
6868
import org.springframework.graphql.data.method.annotation.BatchMapping;
6969
import org.springframework.graphql.data.method.annotation.SchemaMapping;
7070
import org.springframework.graphql.data.pagination.CursorStrategy;
71-
import org.springframework.graphql.data.pagination.SortStrategy;
71+
import org.springframework.graphql.data.query.SortStrategy;
7272
import org.springframework.graphql.execution.BatchLoaderRegistry;
7373
import org.springframework.graphql.execution.DataFetcherExceptionResolver;
7474
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
@@ -128,9 +128,6 @@ public class AnnotatedControllerConfigurer implements ApplicationContextAware, I
128128
@Nullable
129129
private CursorStrategy<?> cursorStrategy;
130130

131-
@Nullable
132-
private SortStrategy<?> sortStrategy;
133-
134131
private final List<HandlerMethodArgumentResolver> customArgumentResolvers = new ArrayList<>(8);
135132

136133
@Nullable
@@ -174,16 +171,6 @@ public void setCursorStrategy(@Nullable CursorStrategy<?> cursorStrategy) {
174171
this.cursorStrategy = cursorStrategy;
175172
}
176173

177-
/**
178-
* Configure a {@link SortStrategy} to extract sort details for pagination
179-
* requests. This results in {@link SortMethodArgumentResolver} being added
180-
* as a method argument resolver.
181-
* @since 1.2
182-
*/
183-
public void setSortStrategy(SortStrategy<?> sortStrategy) {
184-
this.sortStrategy = sortStrategy;
185-
}
186-
187174
/**
188175
* Add a {@link HandlerMethodArgumentResolver} for custom controller method
189176
* arguments. Such custom resolvers are ordered after built-in resolvers
@@ -286,13 +273,19 @@ private HandlerMethodArgumentResolverComposite initArgumentResolvers() {
286273
if (this.cursorStrategy != null) {
287274
resolvers.addResolver(createSubrangeMethodArgumentResolver(this.cursorStrategy));
288275
}
289-
if (this.sortStrategy != null) {
290-
resolvers.addResolver(new SortMethodArgumentResolver(this.sortStrategy));
276+
if (springDataPresent) {
277+
try {
278+
resolvers.addResolver(
279+
new SortMethodArgumentResolver(obtainApplicationContext().getBean(SortStrategy.class)));
280+
}
281+
catch (NoSuchBeanDefinitionException ex) {
282+
// ignore
283+
}
291284
}
292285
if (springSecurityPresent) {
286+
ApplicationContext context = obtainApplicationContext();
293287
resolvers.addResolver(new PrincipalMethodArgumentResolver());
294-
BeanResolver beanResolver = new BeanFactoryResolver(obtainApplicationContext());
295-
resolvers.addResolver(new AuthenticationPrincipalArgumentResolver(beanResolver));
288+
resolvers.addResolver(new AuthenticationPrincipalArgumentResolver(new BeanFactoryResolver(context)));
296289
}
297290
if (KotlinDetector.isKotlinPresent()) {
298291
resolvers.addResolver(new ContinuationHandlerMethodArgumentResolver());

spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/SortMethodArgumentResolver.java

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,36 +20,38 @@
2020
import graphql.schema.DataFetchingEnvironment;
2121

2222
import org.springframework.core.MethodParameter;
23+
import org.springframework.data.domain.Sort;
2324
import org.springframework.graphql.data.method.HandlerMethodArgumentResolver;
24-
import org.springframework.graphql.data.pagination.SortStrategy;
25+
import org.springframework.graphql.data.query.SortStrategy;
2526
import org.springframework.util.Assert;
2627

2728

2829
/**
29-
* Resolver for a Sort object decoded with {@link SortStrategy}.
30+
* Resolver for method arguments of type {@link Sort}.
3031
*
3132
* @author Rossen Stoyanchev
3233
* @since 1.2
3334
*/
3435
public class SortMethodArgumentResolver implements HandlerMethodArgumentResolver {
3536

36-
private final SortStrategy<?> sortStrategy;
37+
private final SortStrategy sortStrategy;
3738

3839

39-
public SortMethodArgumentResolver(SortStrategy<?> sortStrategy) {
40+
public SortMethodArgumentResolver(SortStrategy sortStrategy) {
4041
Assert.notNull(sortStrategy, "SortStrategy is required");
4142
this.sortStrategy = sortStrategy;
4243
}
4344

4445

4546
@Override
4647
public boolean supportsParameter(MethodParameter parameter) {
47-
return this.sortStrategy.supports(parameter.getParameterType());
48+
return parameter.getParameterType().equals(Sort.class);
4849
}
4950

5051
@Override
5152
public Object resolveArgument(MethodParameter parameter, DataFetchingEnvironment environment) {
52-
return this.sortStrategy.extract(environment);
53+
Sort sort = this.sortStrategy.extract(environment);
54+
return (sort != null ? sort : Sort.unsorted());
5355
}
5456

5557
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* Copyright 2020-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.graphql.data.query;
18+
19+
20+
import java.util.ArrayList;
21+
import java.util.List;
22+
23+
import graphql.schema.DataFetchingEnvironment;
24+
25+
import org.springframework.data.domain.Sort;
26+
import org.springframework.lang.Nullable;
27+
import org.springframework.util.ObjectUtils;
28+
29+
/**
30+
* Convenient base class for a {@link SortStrategy}. Subclasses help to extract
31+
* the list of sort {@link #getProperties(DataFetchingEnvironment) properties}
32+
* and {@link #getDirection(DataFetchingEnvironment) direction}.
33+
*
34+
* @author Rossen Stoyanchev
35+
* @since 1.2
36+
*/
37+
public abstract class AbstractSortStrategy implements SortStrategy {
38+
39+
@Override
40+
public Sort extract(DataFetchingEnvironment environment) {
41+
List<String> properties = getProperties(environment);
42+
if (!ObjectUtils.isEmpty(properties)) {
43+
Sort.Direction direction = getDirection(environment);
44+
direction = (direction != null ? direction : Sort.DEFAULT_DIRECTION);
45+
List<Sort.Order> sortOrders = new ArrayList<>(properties.size());
46+
for (String property : properties) {
47+
sortOrders.add(new Sort.Order(direction, property));
48+
}
49+
return Sort.by(sortOrders);
50+
}
51+
return null;
52+
}
53+
54+
/**
55+
* Return the sort properties to use, or an empty list if there are none.
56+
*/
57+
protected abstract List<String> getProperties(DataFetchingEnvironment environment);
58+
59+
/**
60+
* Return the sort direction to use, or {@code null}.
61+
*/
62+
@Nullable
63+
protected abstract Sort.Direction getDirection(DataFetchingEnvironment environment);
64+
65+
}

spring-graphql/src/main/java/org/springframework/graphql/data/pagination/SortStrategy.java renamed to spring-graphql/src/main/java/org/springframework/graphql/data/query/SortStrategy.java

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,32 +14,26 @@
1414
* limitations under the License.
1515
*/
1616

17-
package org.springframework.graphql.data.pagination;
17+
package org.springframework.graphql.data.query;
1818

1919

2020
import graphql.schema.DataFetchingEnvironment;
2121

22+
import org.springframework.data.domain.Sort;
2223
import org.springframework.lang.Nullable;
2324

2425
/**
25-
* Strategy to extract sort information from GraphQL request arguments.
26+
* Strategy to extract {@link Sort} details from GraphQL arguments.
2627
*
2728
* @author Rossen Stoyanchev
2829
* @since 1.2
2930
*/
30-
public interface SortStrategy<S> {
31+
public interface SortStrategy {
3132

3233
/**
33-
* Whether this strategy supports the given Sort object target type.
34-
*/
35-
boolean supports(Class<?> targetType);
36-
37-
/**
38-
* Return an Object that contains sort order and direction information.
39-
* @param environment the environment to obtain GraphQL request arguments from
40-
* @return the object with sort details, if present
34+
* Return a {@link Sort} instance initialized from GraphQL arguments, or {@code null}.
4135
*/
4236
@Nullable
43-
S extract(DataFetchingEnvironment environment);
37+
Sort extract(DataFetchingEnvironment environment);
4438

4539
}

spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/AnnotatedControllerConfigurerTests.java

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import org.springframework.context.support.StaticApplicationContext;
2323
import org.springframework.graphql.data.method.HandlerMethodArgumentResolver;
24+
import org.springframework.graphql.data.query.SortStrategy;
2425

2526
import static org.assertj.core.api.Assertions.assertThat;
2627
import static org.mockito.Mockito.mock;
@@ -33,7 +34,6 @@
3334
*/
3435
public class AnnotatedControllerConfigurerTests {
3536

36-
3737
@Test
3838
void customArgumentResolvers() {
3939
HandlerMethodArgumentResolver customResolver1 = mock(HandlerMethodArgumentResolver.class);
@@ -52,4 +52,29 @@ void customArgumentResolvers() {
5252
assertThat(resolvers).element(size -3).isSameAs(customResolver1);
5353
}
5454

55+
@Test
56+
void sortArgumentResolver() {
57+
SortStrategy sortStrategy = mock(SortStrategy.class);
58+
59+
StaticApplicationContext context = new StaticApplicationContext();
60+
context.registerBean(SortStrategy.class, () -> sortStrategy);
61+
62+
AnnotatedControllerConfigurer configurer = new AnnotatedControllerConfigurer();
63+
configurer.setApplicationContext(context);
64+
configurer.afterPropertiesSet();
65+
66+
List<HandlerMethodArgumentResolver> resolvers = configurer.getArgumentResolvers().getResolvers();
67+
assertThat(resolvers.stream().filter(r -> r instanceof SortMethodArgumentResolver).findFirst()).isPresent();
68+
}
69+
70+
@Test
71+
void sortArgumentResolverStrategyNotPresent() {
72+
AnnotatedControllerConfigurer configurer = new AnnotatedControllerConfigurer();
73+
configurer.setApplicationContext(new StaticApplicationContext());
74+
configurer.afterPropertiesSet();
75+
76+
List<HandlerMethodArgumentResolver> resolvers = configurer.getArgumentResolvers().getResolvers();
77+
assertThat(resolvers.stream().filter(r -> r instanceof SortMethodArgumentResolver).findFirst()).isNotPresent();
78+
}
79+
5580
}

spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/ArgumentResolverTestSupport.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,8 @@ protected DataFetchingEnvironment environment(String argumentsJson) throws JsonP
5757
return DataFetchingEnvironmentImpl.newDataFetchingEnvironment().arguments(arguments).build();
5858
}
5959

60+
protected DataFetchingEnvironment environment(Map<String, Object> arguments) {
61+
return DataFetchingEnvironmentImpl.newDataFetchingEnvironment().arguments(arguments).build();
62+
}
63+
6064
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/*
2+
* Copyright 2002-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.graphql.data.method.annotation.support;
18+
19+
import java.util.List;
20+
import java.util.stream.Collectors;
21+
22+
import graphql.schema.DataFetchingEnvironment;
23+
import org.junit.jupiter.api.Test;
24+
25+
import org.springframework.core.MethodParameter;
26+
import org.springframework.data.domain.Sort;
27+
import org.springframework.graphql.Book;
28+
import org.springframework.graphql.BookCriteria;
29+
import org.springframework.graphql.data.method.annotation.QueryMapping;
30+
import org.springframework.graphql.data.query.AbstractSortStrategy;
31+
import org.springframework.graphql.data.query.SortStrategy;
32+
33+
import static org.assertj.core.api.Assertions.assertThat;
34+
35+
/**
36+
* Unit tests for {@link SortMethodArgumentResolver}.
37+
*
38+
* @author Rossen Stoyanchev
39+
*/
40+
public class SortMethodArgumentResolverTests extends ArgumentResolverTestSupport {
41+
42+
private final MethodParameter param = methodParam(BookController.class, "getBooks", Sort.class);
43+
44+
45+
@Test
46+
void supports() {
47+
SortMethodArgumentResolver resolver = resolver(new SimpleSortStrategy());
48+
assertThat(resolver.supportsParameter(this.param)).isTrue();
49+
50+
MethodParameter param = methodParam(BookController.class, "getBooksByCriteria", BookCriteria.class);
51+
assertThat(resolver.supportsParameter(param)).isFalse();
52+
}
53+
54+
@Test
55+
void resolve() throws Exception {
56+
DataFetchingEnvironment environment = environment("""
57+
{ "sortFields": ["firstName", "lastName", "id"], "sortDirection": "DESC"}"
58+
""");
59+
60+
Sort sort = (Sort) resolver(new SimpleSortStrategy()).resolveArgument(param, environment);
61+
62+
assertThat(sort.stream().collect(Collectors.toList()))
63+
.hasSize(3)
64+
.containsExactly(
65+
new Sort.Order(Sort.Direction.DESC, "firstName"),
66+
new Sort.Order(Sort.Direction.DESC, "lastName"),
67+
new Sort.Order(Sort.Direction.DESC, "id"));
68+
}
69+
70+
private SortMethodArgumentResolver resolver(SortStrategy sortStrategy) {
71+
return new SortMethodArgumentResolver(sortStrategy);
72+
}
73+
74+
75+
@SuppressWarnings({"DataFlowIssue", "unused"})
76+
private static class BookController {
77+
78+
@QueryMapping
79+
public List<Book> getBooks(Sort sort) {
80+
return null;
81+
}
82+
83+
@QueryMapping
84+
public List<Book> getBooksByCriteria(BookCriteria criteria) {
85+
return null;
86+
}
87+
88+
}
89+
90+
91+
private static class SimpleSortStrategy extends AbstractSortStrategy {
92+
93+
@Override
94+
protected List<String> getProperties(DataFetchingEnvironment environment) {
95+
return environment.getArgument("sortFields");
96+
}
97+
98+
@Override
99+
protected Sort.Direction getDirection(DataFetchingEnvironment environment) {
100+
return (environment.containsArgument("sortDirection") ?
101+
Sort.Direction.valueOf(environment.getArgument("sortDirection")) : null);
102+
}
103+
104+
}
105+
106+
}

0 commit comments

Comments
 (0)