Skip to content

Commit a93c2bf

Browse files
committed
DATACMNS-330, DATACMNS-331 - Advanced web integration.
Added PagedResourcesAssembler to easily convert Page instances into a PagedResource instance and automatically build the required previous/next links based on the PageableHandlerMethodArgumentResolver present in the MVC configuration. The assembler can either be injected into a Spring MVC controller or a controller method. The latter will then assume the controller methods URI to be used as pagination link base. Added @EnableSpringDataWebSupport to automatically register HandlerMethodArgumentResolvers for Pageable and Sort as well as a DomainClassConverter that will allow the usage of domain types being managed by Spring Data repositories in controller method signatures. In case Spring HATEOAS is present on the classpath, we'll also register a PagedResourcesAssembler for injection as well as the appropriate HandlerMethodArgumentResolver for injection into controller methods. Adapted Sonargraph configuration accordingly. Upgraded to Spring HATEOAS 0.6.0.BUILD-SNAPSHOT.
1 parent a741651 commit a93c2bf

15 files changed

+1182
-298
lines changed

Spring Data Commons.sonargraph

Lines changed: 299 additions & 293 deletions
Large diffs are not rendered by default.

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
<properties>
2020
<jackson>1.9.7</jackson>
21-
<springhateoas>0.5.0.BUILD-SNAPSHOT</springhateoas>
21+
<springhateoas>0.6.0.BUILD-SNAPSHOT</springhateoas>
2222
<dist.key>DATACMNS</dist.key>
2323
</properties>
2424

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ private String getParameterNameToUse(String source, MethodParameter parameter) {
218218

219219
StringBuilder builder = new StringBuilder(prefix);
220220

221-
if (parameter.hasParameterAnnotation(Qualifier.class)) {
221+
if (parameter != null && parameter.hasParameterAnnotation(Qualifier.class)) {
222222
builder.append(parameter.getParameterAnnotation(Qualifier.class).value());
223223
builder.append(qualifierSeparator);
224224
}
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
/*
2+
* Copyright 2013 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+
* http://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+
package org.springframework.data.web;
17+
18+
import static org.springframework.web.util.UriComponentsBuilder.*;
19+
20+
import java.util.ArrayList;
21+
import java.util.List;
22+
23+
import org.springframework.data.domain.Page;
24+
import org.springframework.data.domain.Pageable;
25+
import org.springframework.hateoas.Link;
26+
import org.springframework.hateoas.PagedResources;
27+
import org.springframework.hateoas.PagedResources.PageMetadata;
28+
import org.springframework.hateoas.Resource;
29+
import org.springframework.hateoas.ResourceAssembler;
30+
import org.springframework.hateoas.ResourceSupport;
31+
import org.springframework.util.Assert;
32+
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
33+
import org.springframework.web.util.UriComponents;
34+
import org.springframework.web.util.UriComponentsBuilder;
35+
36+
/**
37+
* {@link ResourceAssembler} to easily convert {@link Page} instances into {@link PagedResources}.
38+
*
39+
* @since 1.6
40+
* @author Oliver Gierke
41+
*/
42+
public class PagedResourcesAssembler<T> implements ResourceAssembler<Page<T>, PagedResources<Resource<T>>> {
43+
44+
private final PageableHandlerMethodArgumentResolver pageableResolver;
45+
private final UriComponents baseUri;
46+
47+
/**
48+
* Creates a new {@link PagedResourcesAssembler} using the given {@link PageableHandlerMethodArgumentResolver} and
49+
* base URI. If the former is {@literal null}, a default one will be created. If the latter is {@literal null}, calls
50+
* to {@link #toResource(Page)} will use the current request's URI to build the relevant previous and next links.
51+
*
52+
* @param resolver
53+
* @param baseUri
54+
*/
55+
public PagedResourcesAssembler(PageableHandlerMethodArgumentResolver resolver, UriComponents baseUri) {
56+
57+
this.pageableResolver = resolver == null ? new PageableHandlerMethodArgumentResolver() : resolver;
58+
this.baseUri = baseUri;
59+
}
60+
61+
/*
62+
* (non-Javadoc)
63+
* @see org.springframework.hateoas.ResourceAssembler#toResource(java.lang.Object)
64+
*/
65+
@Override
66+
public PagedResources<Resource<T>> toResource(Page<T> entity) {
67+
return toResource(entity, new SimplePagedResourceAssembler<T>());
68+
}
69+
70+
/**
71+
* Creates a new {@link PagedResources} by converting the given {@link Page} into a {@link PageMetadata} instance and
72+
* wrapping the contained elements into {@link Resource} instances. Will add pagination links based on the given the
73+
* self link.
74+
*
75+
* @param page must not be {@literal null}.
76+
* @param selfLink must not be {@literal null}.
77+
* @return
78+
*/
79+
public PagedResources<Resource<T>> toResource(Page<T> page, Link selfLink) {
80+
return toResource(page, new SimplePagedResourceAssembler<T>(), selfLink);
81+
}
82+
83+
/**
84+
* Creates a new {@link PagedResources} by converting the given {@link Page} into a {@link PageMetadata} instance and
85+
* using the given {@link ResourceAssembler} to turn elements of the {@link Page} into resources.
86+
*
87+
* @param page must not be {@literal null}.
88+
* @param assembler must not be {@literal null}.
89+
* @return
90+
*/
91+
public <R extends ResourceSupport> PagedResources<R> toResource(Page<T> page, ResourceAssembler<T, R> assembler) {
92+
return createResource(page, assembler, null);
93+
}
94+
95+
/**
96+
* Creates a new {@link PagedResources} by converting the given {@link Page} into a {@link PageMetadata} instance and
97+
* using the given {@link ResourceAssembler} to turn elements of the {@link Page} into resources. Will add pagination
98+
* links based on the given the self link.
99+
*
100+
* @param page must not be {@literal null}.
101+
* @param assembler must not be {@literal null}.
102+
* @param link must not be {@literal null}.
103+
* @return
104+
*/
105+
public <R extends ResourceSupport> PagedResources<R> toResource(Page<T> page, ResourceAssembler<T, R> assembler,
106+
Link link) {
107+
108+
Assert.notNull(link, "Link must not be null!");
109+
return createResource(page, assembler, link);
110+
}
111+
112+
private <S, R extends ResourceSupport> PagedResources<R> createResource(Page<S> page,
113+
ResourceAssembler<S, R> assembler, Link link) {
114+
115+
Assert.notNull(page, "Page must not be null!");
116+
Assert.notNull(assembler, "ResourceAssembler must not be null!");
117+
118+
List<R> resources = new ArrayList<R>(page.getNumberOfElements());
119+
120+
for (S element : page) {
121+
resources.add(assembler.toResource(element));
122+
}
123+
124+
PagedResources<R> pagedResources = new PagedResources<R>(resources, asPageMetadata(page));
125+
return addPaginationLinks(pagedResources, page, link == null ? getDefaultUriString().toUriString() : link.getHref());
126+
}
127+
128+
private UriComponents getDefaultUriString() {
129+
return baseUri == null ? ServletUriComponentsBuilder.fromCurrentRequest().build() : baseUri;
130+
}
131+
132+
private <R extends ResourceSupport> PagedResources<R> addPaginationLinks(PagedResources<R> resources, Page<?> page,
133+
String uri) {
134+
135+
if (page.hasNextPage()) {
136+
foo(resources, page.nextPageable(), uri, Link.REL_NEXT);
137+
}
138+
139+
if (page.hasPreviousPage()) {
140+
foo(resources, page.previousPageable(), uri, Link.REL_PREVIOUS);
141+
}
142+
143+
return resources;
144+
}
145+
146+
private void foo(PagedResources<?> resources, Pageable pageable, String uri, String rel) {
147+
148+
UriComponentsBuilder builder = fromUriString(uri);
149+
pageableResolver.enhance(builder, null, pageable);
150+
resources.add(new Link(builder.build().toUriString(), rel));
151+
}
152+
153+
/**
154+
* Creates a new {@link PageMetadata} instance from the given {@link Page}.
155+
*
156+
* @param page must not be {@literal null}.
157+
* @return
158+
*/
159+
private static <T> PageMetadata asPageMetadata(Page<T> page) {
160+
161+
Assert.notNull(page, "Page must not be null!");
162+
return new PageMetadata(page.getSize(), page.getNumber(), page.getTotalElements(), page.getTotalPages());
163+
}
164+
165+
private static class SimplePagedResourceAssembler<T> implements ResourceAssembler<T, Resource<T>> {
166+
167+
@Override
168+
public Resource<T> toResource(T entity) {
169+
return new Resource<T>(entity);
170+
}
171+
}
172+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* Copyright 2013 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+
* http://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+
package org.springframework.data.web;
17+
18+
import org.springframework.core.MethodParameter;
19+
import org.springframework.hateoas.Link;
20+
import org.springframework.hateoas.MethodLinkBuilderFactory;
21+
import org.springframework.hateoas.mvc.ControllerLinkBuilderFactory;
22+
import org.springframework.web.bind.support.WebDataBinderFactory;
23+
import org.springframework.web.context.request.NativeWebRequest;
24+
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
25+
import org.springframework.web.method.support.ModelAndViewContainer;
26+
import org.springframework.web.util.UriComponents;
27+
import org.springframework.web.util.UriComponentsBuilder;
28+
29+
/**
30+
* {@link HandlerMethodArgumentResolver} to allow injection of {@link PagedResourcesAssembler} into Spring MVC
31+
* controller methods.
32+
*
33+
* @since 1.6
34+
* @author Oliver Gierke
35+
*/
36+
public class PagedResourcesAssemblerArgumentResolver implements HandlerMethodArgumentResolver {
37+
38+
private final PageableHandlerMethodArgumentResolver resolver;
39+
private final MethodLinkBuilderFactory<?> linkBuilderFactory;
40+
41+
/**
42+
* Creates a new {@link PagedResourcesAssemblerArgumentResolver} using the given
43+
* {@link PageableHandlerMethodArgumentResolver} and {@link MethodLinkBuilderFactory}.
44+
*
45+
* @param resolver can be {@literal null}.
46+
* @param linkBuilderFactory can be {@literal null}, will be defaulted to a {@link ControllerLinkBuilderFactory}.
47+
*/
48+
public PagedResourcesAssemblerArgumentResolver(PageableHandlerMethodArgumentResolver resolver,
49+
MethodLinkBuilderFactory<?> linkBuilderFactory) {
50+
51+
this.resolver = resolver;
52+
this.linkBuilderFactory = linkBuilderFactory == null ? new ControllerLinkBuilderFactory() : linkBuilderFactory;
53+
}
54+
55+
/*
56+
* (non-Javadoc)
57+
* @see org.springframework.web.method.support.HandlerMethodArgumentResolver#supportsParameter(org.springframework.core.MethodParameter)
58+
*/
59+
@Override
60+
public boolean supportsParameter(MethodParameter parameter) {
61+
return PagedResourcesAssembler.class.equals(parameter.getParameterType());
62+
}
63+
64+
/*
65+
* (non-Javadoc)
66+
* @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)
67+
*/
68+
@Override
69+
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
70+
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
71+
72+
Link linkToMethod = linkBuilderFactory.linkTo(parameter.getMethod(), new Object[0]).withSelfRel();
73+
UriComponents fromUriString = UriComponentsBuilder.fromUriString(linkToMethod.getHref()).build();
74+
75+
return new PagedResourcesAssembler<Object>(resolver, fromUriString);
76+
}
77+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ private String getSortParameter(MethodParameter parameter) {
218218

219219
StringBuilder builder = new StringBuilder();
220220

221-
if (parameter.hasParameterAnnotation(Qualifier.class)) {
221+
if (parameter != null && parameter.hasParameterAnnotation(Qualifier.class)) {
222222
builder.append(parameter.getParameterAnnotation(Qualifier.class).value()).append(qualifierDelimiter);
223223
}
224224

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* Copyright 2013 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+
* http://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+
package org.springframework.data.web.config;
17+
18+
import java.lang.annotation.ElementType;
19+
import java.lang.annotation.Inherited;
20+
import java.lang.annotation.Retention;
21+
import java.lang.annotation.RetentionPolicy;
22+
import java.lang.annotation.Target;
23+
import java.util.ArrayList;
24+
import java.util.List;
25+
26+
import org.springframework.context.annotation.Import;
27+
import org.springframework.context.annotation.ImportSelector;
28+
import org.springframework.core.type.AnnotationMetadata;
29+
import org.springframework.data.domain.Pageable;
30+
import org.springframework.data.domain.Sort;
31+
import org.springframework.data.repository.support.DomainClassConverter;
32+
import org.springframework.data.web.PageableHandlerMethodArgumentResolver;
33+
import org.springframework.data.web.PagedResourcesAssembler;
34+
import org.springframework.data.web.PagedResourcesAssemblerArgumentResolver;
35+
import org.springframework.data.web.SortHandlerMethodArgumentResolver;
36+
import org.springframework.util.ClassUtils;
37+
import org.springframework.web.bind.annotation.PathVariable;
38+
import org.springframework.web.bind.annotation.RequestParam;
39+
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
40+
41+
/**
42+
* Annotation to automatically register the following beans for usage with Spring MVC.
43+
* <ul>
44+
* <li>{@link DomainClassConverter} - to allow usage of domain types managed by Spring Data repositories as controller
45+
* method arguments bound with {@link PathVariable} or {@link RequestParam}.</li>
46+
* <li>{@link PageableHandlerMethodArgumentResolver} - to allow injection of {@link Pageable} instances into controller
47+
* methods automatically created from request parameters.</li>
48+
* <li>{@link SortHandlerMethodArgumentResolver} - to allow injection of {@link Sort} instances into controller methods
49+
* automatically created from request parameters.</li>
50+
* </ul>
51+
* If Spring HATEOAS is present on the classpath we will additionall register the following beans:
52+
* <ul>
53+
* <li>{@link PagedResourcesAssembler} - for injection into web components</li>
54+
* <li>{@link PagedResourcesAssemblerArgumentResolver} - for injection of {@link PagedResourcesAssembler} into
55+
* controller methods</li>
56+
* <ul>
57+
*
58+
* @since 1.6
59+
* @see SpringDataWebConfiguration
60+
* @see HateoasAwareSpringDataWebConfiguration
61+
* @author Oliver Gierke
62+
*/
63+
@Retention(RetentionPolicy.RUNTIME)
64+
@Target({ ElementType.TYPE, ElementType.ANNOTATION_TYPE })
65+
@Inherited
66+
@Import(EnableSpringDataWebSupport.SpringDataWebConfigurationImportSelector.class)
67+
public @interface EnableSpringDataWebSupport {
68+
69+
/**
70+
* Import selector to import the appropriate configuration class depending on whether Spring HATEOAS is present on the
71+
* classpath. We need to register the HATEOAS specific class first as apparently only the first class implementing
72+
* {@link WebMvcConfigurationSupport} gets callbacks invoked (see https://jira.springsource.org/browse/SPR-10565).
73+
*
74+
* @author Oliver Gierke
75+
*/
76+
class SpringDataWebConfigurationImportSelector implements ImportSelector {
77+
78+
// Don't make final to allow test cases faking this to false
79+
private static boolean HATEOAS_PRESENT = ClassUtils.isPresent("org.springframework.hateoas.Link", null);
80+
81+
/*
82+
* (non-Javadoc)
83+
* @see org.springframework.context.annotation.ImportSelector#selectImports(org.springframework.core.type.AnnotationMetadata)
84+
*/
85+
@Override
86+
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
87+
88+
List<String> configs = new ArrayList<String>();
89+
90+
if (HATEOAS_PRESENT) {
91+
configs.add(HateoasAwareSpringDataWebConfiguration.class.getName());
92+
}
93+
94+
configs.add(SpringDataWebConfiguration.class.getName());
95+
return configs.toArray(new String[configs.size()]);
96+
}
97+
}
98+
}

0 commit comments

Comments
 (0)