From a93c2bff9456da09eb25fa51dbfb452a74d64883 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Wed, 8 May 2013 14:07:13 +0200 Subject: [PATCH] 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. --- Spring Data Commons.sonargraph | 586 +++++++++--------- pom.xml | 2 +- ...PageableHandlerMethodArgumentResolver.java | 2 +- .../data/web/PagedResourcesAssembler.java | 172 +++++ ...gedResourcesAssemblerArgumentResolver.java | 77 +++ .../SortHandlerMethodArgumentResolver.java | 2 +- .../config/EnableSpringDataWebSupport.java | 98 +++ ...ateoasAwareSpringDataWebConfiguration.java | 59 ++ .../config/SpringDataWebConfiguration.java | 64 ++ .../data/web/config/package-info.java | 20 + .../web/PagedResourcesAssemblerUnitTests.java | 111 ++++ .../data/web/WebTestUtils.java | 61 ++ ...eSpringDataWebSupportIntegrationTests.java | 121 ++++ ...bleResourcesAssemblerIntegrationTests.java | 96 +++ template.mf | 3 +- 15 files changed, 1179 insertions(+), 295 deletions(-) create mode 100644 src/main/java/org/springframework/data/web/PagedResourcesAssembler.java create mode 100644 src/main/java/org/springframework/data/web/PagedResourcesAssemblerArgumentResolver.java create mode 100644 src/main/java/org/springframework/data/web/config/EnableSpringDataWebSupport.java create mode 100644 src/main/java/org/springframework/data/web/config/HateoasAwareSpringDataWebConfiguration.java create mode 100644 src/main/java/org/springframework/data/web/config/SpringDataWebConfiguration.java create mode 100644 src/main/java/org/springframework/data/web/config/package-info.java create mode 100644 src/test/java/org/springframework/data/web/PagedResourcesAssemblerUnitTests.java create mode 100644 src/test/java/org/springframework/data/web/WebTestUtils.java create mode 100644 src/test/java/org/springframework/data/web/config/EnableSpringDataWebSupportIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/web/config/PageableResourcesAssemblerIntegrationTests.java diff --git a/Spring Data Commons.sonargraph b/Spring Data Commons.sonargraph index a06757e41..78949deee 100644 --- a/Spring Data Commons.sonargraph +++ b/Spring Data Commons.sonargraph @@ -1,424 +1,430 @@ - - + + - - - + + + - - - - - - + + + + + + + + + + + + - - - + + + - - - + + + - + - - - - + + + + - - - + + + - - + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - - + + + + - - - + + + - + - - - + + + - - - + + + - - - + + + - - + + - - - + + + - - - - + + + + - - - + + + - + - + - - - - - + + + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - + - - - - - + + + + + - - - + + + - - - + + + - - - + + + - + - - - + + + - + - + - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - - + + + + - - - + + + - - - + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - - - - - + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pom.xml b/pom.xml index e13a02967..602792117 100644 --- a/pom.xml +++ b/pom.xml @@ -18,7 +18,7 @@ 1.9.7 - 0.5.0.BUILD-SNAPSHOT + 0.6.0.BUILD-SNAPSHOT DATACMNS diff --git a/src/main/java/org/springframework/data/web/PageableHandlerMethodArgumentResolver.java b/src/main/java/org/springframework/data/web/PageableHandlerMethodArgumentResolver.java index 048b6349e..8e834eac1 100644 --- a/src/main/java/org/springframework/data/web/PageableHandlerMethodArgumentResolver.java +++ b/src/main/java/org/springframework/data/web/PageableHandlerMethodArgumentResolver.java @@ -218,7 +218,7 @@ public class PageableHandlerMethodArgumentResolver implements HandlerMethodArgum StringBuilder builder = new StringBuilder(prefix); - if (parameter.hasParameterAnnotation(Qualifier.class)) { + if (parameter != null && parameter.hasParameterAnnotation(Qualifier.class)) { builder.append(parameter.getParameterAnnotation(Qualifier.class).value()); builder.append(qualifierSeparator); } diff --git a/src/main/java/org/springframework/data/web/PagedResourcesAssembler.java b/src/main/java/org/springframework/data/web/PagedResourcesAssembler.java new file mode 100644 index 000000000..7afe0dd85 --- /dev/null +++ b/src/main/java/org/springframework/data/web/PagedResourcesAssembler.java @@ -0,0 +1,172 @@ +/* + * 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.web.util.UriComponentsBuilder.*; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.hateoas.Link; +import org.springframework.hateoas.PagedResources; +import org.springframework.hateoas.PagedResources.PageMetadata; +import org.springframework.hateoas.Resource; +import org.springframework.hateoas.ResourceAssembler; +import org.springframework.hateoas.ResourceSupport; +import org.springframework.util.Assert; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; + +/** + * {@link ResourceAssembler} to easily convert {@link Page} instances into {@link PagedResources}. + * + * @since 1.6 + * @author Oliver Gierke + */ +public class PagedResourcesAssembler implements ResourceAssembler, PagedResources>> { + + private final PageableHandlerMethodArgumentResolver pageableResolver; + private final UriComponents baseUri; + + /** + * Creates a new {@link PagedResourcesAssembler} using the given {@link PageableHandlerMethodArgumentResolver} and + * base URI. If the former is {@literal null}, a default one will be created. If the latter is {@literal null}, calls + * to {@link #toResource(Page)} will use the current request's URI to build the relevant previous and next links. + * + * @param resolver + * @param baseUri + */ + public PagedResourcesAssembler(PageableHandlerMethodArgumentResolver resolver, UriComponents baseUri) { + + this.pageableResolver = resolver == null ? new PageableHandlerMethodArgumentResolver() : resolver; + this.baseUri = baseUri; + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.ResourceAssembler#toResource(java.lang.Object) + */ + @Override + public PagedResources> toResource(Page entity) { + return toResource(entity, new SimplePagedResourceAssembler()); + } + + /** + * Creates a new {@link PagedResources} by converting the given {@link Page} into a {@link PageMetadata} instance and + * wrapping the contained elements into {@link Resource} instances. Will add pagination links based on the given the + * self link. + * + * @param page must not be {@literal null}. + * @param selfLink must not be {@literal null}. + * @return + */ + public PagedResources> toResource(Page page, Link selfLink) { + return toResource(page, new SimplePagedResourceAssembler(), selfLink); + } + + /** + * Creates a new {@link PagedResources} by converting the given {@link Page} into a {@link PageMetadata} instance and + * using the given {@link ResourceAssembler} to turn elements of the {@link Page} into resources. + * + * @param page must not be {@literal null}. + * @param assembler must not be {@literal null}. + * @return + */ + public PagedResources toResource(Page page, ResourceAssembler assembler) { + return createResource(page, assembler, null); + } + + /** + * Creates a new {@link PagedResources} by converting the given {@link Page} into a {@link PageMetadata} instance and + * using the given {@link ResourceAssembler} to turn elements of the {@link Page} into resources. Will add pagination + * links based on the given the self link. + * + * @param page must not be {@literal null}. + * @param assembler must not be {@literal null}. + * @param link must not be {@literal null}. + * @return + */ + public PagedResources toResource(Page page, ResourceAssembler assembler, + Link link) { + + Assert.notNull(link, "Link must not be null!"); + return createResource(page, assembler, link); + } + + private PagedResources createResource(Page page, + ResourceAssembler assembler, Link link) { + + Assert.notNull(page, "Page must not be null!"); + Assert.notNull(assembler, "ResourceAssembler must not be null!"); + + List resources = new ArrayList(page.getNumberOfElements()); + + for (S element : page) { + resources.add(assembler.toResource(element)); + } + + PagedResources pagedResources = new PagedResources(resources, asPageMetadata(page)); + return addPaginationLinks(pagedResources, page, link == null ? getDefaultUriString().toUriString() : link.getHref()); + } + + private UriComponents getDefaultUriString() { + return baseUri == null ? ServletUriComponentsBuilder.fromCurrentRequest().build() : baseUri; + } + + private PagedResources addPaginationLinks(PagedResources resources, Page page, + String uri) { + + if (page.hasNextPage()) { + foo(resources, page.nextPageable(), uri, Link.REL_NEXT); + } + + if (page.hasPreviousPage()) { + foo(resources, page.previousPageable(), uri, Link.REL_PREVIOUS); + } + + return resources; + } + + private void foo(PagedResources resources, Pageable pageable, String uri, String rel) { + + UriComponentsBuilder builder = fromUriString(uri); + pageableResolver.enhance(builder, null, pageable); + resources.add(new Link(builder.build().toUriString(), rel)); + } + + /** + * Creates a new {@link PageMetadata} instance from the given {@link Page}. + * + * @param page must not be {@literal null}. + * @return + */ + private static PageMetadata asPageMetadata(Page page) { + + Assert.notNull(page, "Page must not be null!"); + return new PageMetadata(page.getSize(), page.getNumber(), page.getTotalElements(), page.getTotalPages()); + } + + private static class SimplePagedResourceAssembler implements ResourceAssembler> { + + @Override + public Resource toResource(T entity) { + return new Resource(entity); + } + } +} diff --git a/src/main/java/org/springframework/data/web/PagedResourcesAssemblerArgumentResolver.java b/src/main/java/org/springframework/data/web/PagedResourcesAssemblerArgumentResolver.java new file mode 100644 index 000000000..1b38b4e5f --- /dev/null +++ b/src/main/java/org/springframework/data/web/PagedResourcesAssemblerArgumentResolver.java @@ -0,0 +1,77 @@ +/* + * 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.springframework.core.MethodParameter; +import org.springframework.hateoas.Link; +import org.springframework.hateoas.MethodLinkBuilderFactory; +import org.springframework.hateoas.mvc.ControllerLinkBuilderFactory; +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.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; + +/** + * {@link HandlerMethodArgumentResolver} to allow injection of {@link PagedResourcesAssembler} into Spring MVC + * controller methods. + * + * @since 1.6 + * @author Oliver Gierke + */ +public class PagedResourcesAssemblerArgumentResolver implements HandlerMethodArgumentResolver { + + private final PageableHandlerMethodArgumentResolver resolver; + private final MethodLinkBuilderFactory linkBuilderFactory; + + /** + * Creates a new {@link PagedResourcesAssemblerArgumentResolver} using the given + * {@link PageableHandlerMethodArgumentResolver} and {@link MethodLinkBuilderFactory}. + * + * @param resolver can be {@literal null}. + * @param linkBuilderFactory can be {@literal null}, will be defaulted to a {@link ControllerLinkBuilderFactory}. + */ + public PagedResourcesAssemblerArgumentResolver(PageableHandlerMethodArgumentResolver resolver, + MethodLinkBuilderFactory linkBuilderFactory) { + + this.resolver = resolver; + this.linkBuilderFactory = linkBuilderFactory == null ? new ControllerLinkBuilderFactory() : linkBuilderFactory; + } + + /* + * (non-Javadoc) + * @see org.springframework.web.method.support.HandlerMethodArgumentResolver#supportsParameter(org.springframework.core.MethodParameter) + */ + @Override + public boolean supportsParameter(MethodParameter parameter) { + return PagedResourcesAssembler.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 Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + + Link linkToMethod = linkBuilderFactory.linkTo(parameter.getMethod(), new Object[0]).withSelfRel(); + UriComponents fromUriString = UriComponentsBuilder.fromUriString(linkToMethod.getHref()).build(); + + return new PagedResourcesAssembler(resolver, fromUriString); + } +} diff --git a/src/main/java/org/springframework/data/web/SortHandlerMethodArgumentResolver.java b/src/main/java/org/springframework/data/web/SortHandlerMethodArgumentResolver.java index 22d7355e5..b0534f988 100644 --- a/src/main/java/org/springframework/data/web/SortHandlerMethodArgumentResolver.java +++ b/src/main/java/org/springframework/data/web/SortHandlerMethodArgumentResolver.java @@ -218,7 +218,7 @@ public class SortHandlerMethodArgumentResolver implements HandlerMethodArgumentR StringBuilder builder = new StringBuilder(); - if (parameter.hasParameterAnnotation(Qualifier.class)) { + if (parameter != null && parameter.hasParameterAnnotation(Qualifier.class)) { builder.append(parameter.getParameterAnnotation(Qualifier.class).value()).append(qualifierDelimiter); } diff --git a/src/main/java/org/springframework/data/web/config/EnableSpringDataWebSupport.java b/src/main/java/org/springframework/data/web/config/EnableSpringDataWebSupport.java new file mode 100644 index 000000000..e546f6adb --- /dev/null +++ b/src/main/java/org/springframework/data/web/config/EnableSpringDataWebSupport.java @@ -0,0 +1,98 @@ +/* + * 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.config; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.ArrayList; +import java.util.List; + +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.ImportSelector; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.repository.support.DomainClassConverter; +import org.springframework.data.web.PageableHandlerMethodArgumentResolver; +import org.springframework.data.web.PagedResourcesAssembler; +import org.springframework.data.web.PagedResourcesAssemblerArgumentResolver; +import org.springframework.data.web.SortHandlerMethodArgumentResolver; +import org.springframework.util.ClassUtils; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport; + +/** + * Annotation to automatically register the following beans for usage with Spring MVC. + *
    + *
  • {@link DomainClassConverter} - to allow usage of domain types managed by Spring Data repositories as controller + * method arguments bound with {@link PathVariable} or {@link RequestParam}.
  • + *
  • {@link PageableHandlerMethodArgumentResolver} - to allow injection of {@link Pageable} instances into controller + * methods automatically created from request parameters.
  • + *
  • {@link SortHandlerMethodArgumentResolver} - to allow injection of {@link Sort} instances into controller methods + * automatically created from request parameters.
  • + *
+ * If Spring HATEOAS is present on the classpath we will additionall register the following beans: + *
    + *
  • {@link PagedResourcesAssembler} - for injection into web components
  • + *
  • {@link PagedResourcesAssemblerArgumentResolver} - for injection of {@link PagedResourcesAssembler} into + * controller methods
  • + *
      + * + * @since 1.6 + * @see SpringDataWebConfiguration + * @see HateoasAwareSpringDataWebConfiguration + * @author Oliver Gierke + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE, ElementType.ANNOTATION_TYPE }) +@Inherited +@Import(EnableSpringDataWebSupport.SpringDataWebConfigurationImportSelector.class) +public @interface EnableSpringDataWebSupport { + + /** + * Import selector to import the appropriate configuration class depending on whether Spring HATEOAS is present on the + * classpath. We need to register the HATEOAS specific class first as apparently only the first class implementing + * {@link WebMvcConfigurationSupport} gets callbacks invoked (see https://jira.springsource.org/browse/SPR-10565). + * + * @author Oliver Gierke + */ + class SpringDataWebConfigurationImportSelector implements ImportSelector { + + // Don't make final to allow test cases faking this to false + private static boolean HATEOAS_PRESENT = ClassUtils.isPresent("org.springframework.hateoas.Link", null); + + /* + * (non-Javadoc) + * @see org.springframework.context.annotation.ImportSelector#selectImports(org.springframework.core.type.AnnotationMetadata) + */ + @Override + public String[] selectImports(AnnotationMetadata importingClassMetadata) { + + List configs = new ArrayList(); + + if (HATEOAS_PRESENT) { + configs.add(HateoasAwareSpringDataWebConfiguration.class.getName()); + } + + configs.add(SpringDataWebConfiguration.class.getName()); + return configs.toArray(new String[configs.size()]); + } + } +} diff --git a/src/main/java/org/springframework/data/web/config/HateoasAwareSpringDataWebConfiguration.java b/src/main/java/org/springframework/data/web/config/HateoasAwareSpringDataWebConfiguration.java new file mode 100644 index 000000000..80fe951ca --- /dev/null +++ b/src/main/java/org/springframework/data/web/config/HateoasAwareSpringDataWebConfiguration.java @@ -0,0 +1,59 @@ +/* + * 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.config; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.web.PagedResourcesAssembler; +import org.springframework.data.web.PagedResourcesAssemblerArgumentResolver; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport; + +/** + * JavaConfig class to register {@link PagedResourcesAssembler} and {@link PagedResourcesAssemblerArgumentResolver}. + * + * @since 1.6 + * @author Oliver Gierke + */ +@Configuration +class HateoasAwareSpringDataWebConfiguration extends WebMvcConfigurationSupport { + + @Autowired + SpringDataWebConfiguration config; + + @Bean + public PagedResourcesAssembler pagedResourcesAssembler() { + return new PagedResourcesAssembler(config.pageableResolver(), null); + } + + @Bean + public PagedResourcesAssemblerArgumentResolver pagedResourcesAssemblerArgumentResolver() { + return new PagedResourcesAssemblerArgumentResolver(config.pageableResolver(), null); + } + + /* + * (non-Javadoc) + * @see org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#addArgumentResolvers(java.util.List) + */ + @Override + protected void addArgumentResolvers(List argumentResolvers) { + config.addArgumentResolvers(argumentResolvers); + argumentResolvers.add(pagedResourcesAssemblerArgumentResolver()); + } +} diff --git a/src/main/java/org/springframework/data/web/config/SpringDataWebConfiguration.java b/src/main/java/org/springframework/data/web/config/SpringDataWebConfiguration.java new file mode 100644 index 000000000..6bf6617c4 --- /dev/null +++ b/src/main/java/org/springframework/data/web/config/SpringDataWebConfiguration.java @@ -0,0 +1,64 @@ +/* + * 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.config; + +import java.util.List; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.repository.support.DomainClassConverter; +import org.springframework.data.web.PageableHandlerMethodArgumentResolver; +import org.springframework.data.web.SortHandlerMethodArgumentResolver; +import org.springframework.format.support.FormattingConversionService; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport; + +/** + * Configuration class to register {@link PageableHandlerMethodArgumentResolver}, + * {@link SortHandlerMethodArgumentResolver} and {@link DomainClassConverter}. + * + * @since 1.6 + * @author Oliver Gierke + */ +@Configuration +class SpringDataWebConfiguration extends WebMvcConfigurationSupport { + + @Bean + public PageableHandlerMethodArgumentResolver pageableResolver() { + return new PageableHandlerMethodArgumentResolver(); + } + + @Bean + public SortHandlerMethodArgumentResolver sortResolver() { + return new SortHandlerMethodArgumentResolver(); + } + + @Bean + public DomainClassConverter mvcDomainClassConverter() { + return new DomainClassConverter(mvcConversionService()); + } + + /* + * (non-Javadoc) + * @see org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter#addArgumentResolvers(java.util.List) + */ + @Override + public void addArgumentResolvers(List argumentResolvers) { + + argumentResolvers.add(pageableResolver()); + argumentResolvers.add(sortResolver()); + } +} diff --git a/src/main/java/org/springframework/data/web/config/package-info.java b/src/main/java/org/springframework/data/web/config/package-info.java new file mode 100644 index 000000000..695a28ab4 --- /dev/null +++ b/src/main/java/org/springframework/data/web/config/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ +/** + * + * @author Oliver Gierke + */ +package org.springframework.data.web.config; \ No newline at end of file diff --git a/src/test/java/org/springframework/data/web/PagedResourcesAssemblerUnitTests.java b/src/test/java/org/springframework/data/web/PagedResourcesAssemblerUnitTests.java new file mode 100644 index 000000000..9287971b1 --- /dev/null +++ b/src/test/java/org/springframework/data/web/PagedResourcesAssemblerUnitTests.java @@ -0,0 +1,111 @@ +/* + * 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 java.util.Arrays; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.hateoas.Link; +import org.springframework.hateoas.PagedResources; +import org.springframework.hateoas.Resource; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; + +/** + * Unit tests for {@link PagedResourcesAssembler}. + * + * @author Oliver Gierke + */ +public class PagedResourcesAssemblerUnitTests { + + PageableHandlerMethodArgumentResolver resolver = new PageableHandlerMethodArgumentResolver(); + + @Before + public void setUp() { + WebTestUtils.initWebTest(); + } + + @Test + public void addsNextLinkForFirstPage() { + + PagedResourcesAssembler assembler = new PagedResourcesAssembler(resolver, null); + PagedResources> resources = assembler.toResource(createPage(0)); + + assertThat(resources.getLink(Link.REL_PREVIOUS), is(nullValue())); + assertThat(resources.getLink(Link.REL_NEXT), is(notNullValue())); + } + + @Test + public void addsPreviousAndNextLinksForMiddlePage() { + + PagedResourcesAssembler assembler = new PagedResourcesAssembler(resolver, null); + PagedResources> resources = assembler.toResource(createPage(1)); + + assertThat(resources.getLink(Link.REL_PREVIOUS), is(notNullValue())); + assertThat(resources.getLink(Link.REL_NEXT), is(notNullValue())); + } + + @Test + public void addsPreviousLinkForLastPage() { + + PagedResourcesAssembler assembler = new PagedResourcesAssembler(resolver, null); + PagedResources> resources = assembler.toResource(createPage(2)); + + assertThat(resources.getLink(Link.REL_PREVIOUS), is(notNullValue())); + assertThat(resources.getLink(Link.REL_NEXT), is(nullValue())); + } + + @Test + public void usesBaseUriIfConfigured() { + + UriComponents baseUri = UriComponentsBuilder.fromUriString("http://foo:9090").build(); + + PagedResourcesAssembler assembler = new PagedResourcesAssembler(resolver, baseUri); + PagedResources> resources = assembler.toResource(createPage(1)); + + assertThat(resources.getLink(Link.REL_PREVIOUS).getHref(), startsWith(baseUri.toUriString())); + assertThat(resources.getLink(Link.REL_NEXT).getHref(), startsWith(baseUri.toUriString())); + } + + @Test + public void usesCustomLinkProvided() { + + Link link = new Link("http://foo:9090", "rel"); + + PagedResourcesAssembler assembler = new PagedResourcesAssembler(resolver, null); + PagedResources> resources = assembler.toResource(createPage(1), link); + + assertThat(resources.getLink(Link.REL_PREVIOUS).getHref(), startsWith(link.getHref())); + assertThat(resources.getLink(Link.REL_NEXT).getHref(), startsWith(link.getHref())); + } + + private static Page createPage(int index) { + + PageRequest request = new PageRequest(index, 1); + return new PageImpl(Arrays.asList(new Person()), request, 3); + } + + static class Person { + + } +} diff --git a/src/test/java/org/springframework/data/web/WebTestUtils.java b/src/test/java/org/springframework/data/web/WebTestUtils.java new file mode 100644 index 000000000..2c8c6a0df --- /dev/null +++ b/src/test/java/org/springframework/data/web/WebTestUtils.java @@ -0,0 +1,61 @@ +/* + * 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.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockServletContext; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; + +/** + * Helper methods for web integration testing. + * + * @author Oliver Gierke + */ +public class WebTestUtils { + + /** + * Initializes web tests. Will register a {@link MockHttpServletRequest} for the current thread. + */ + public static void initWebTest() { + + MockHttpServletRequest request = new MockHttpServletRequest(); + ServletRequestAttributes requestAttributes = new ServletRequestAttributes(request); + RequestContextHolder.setRequestAttributes(requestAttributes); + } + + /** + * Creates a {@link WebApplicationContext} from the given configuration classes. + * + * @param configClasses + * @return + */ + public static WebApplicationContext createApplicationContext(Class... configClasses) { + + AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); + context.setServletContext(new MockServletContext()); + + for (Class configClass : configClasses) { + context.register(configClass); + } + + context.refresh(); + + return context; + } +} diff --git a/src/test/java/org/springframework/data/web/config/EnableSpringDataWebSupportIntegrationTests.java b/src/test/java/org/springframework/data/web/config/EnableSpringDataWebSupportIntegrationTests.java new file mode 100644 index 000000000..c6f09dc35 --- /dev/null +++ b/src/test/java/org/springframework/data/web/config/EnableSpringDataWebSupportIntegrationTests.java @@ -0,0 +1,121 @@ +/* + * 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.config; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.hamcrest.Matcher; +import org.junit.After; +import org.junit.Test; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.web.PageableHandlerMethodArgumentResolver; +import org.springframework.data.web.PagedResourcesAssemblerArgumentResolver; +import org.springframework.data.web.SortHandlerMethodArgumentResolver; +import org.springframework.data.web.WebTestUtils; +import org.springframework.data.web.config.EnableSpringDataWebSupport.SpringDataWebConfigurationImportSelector; +import org.springframework.util.ReflectionUtils; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; + +/** + * Integration tests for {@link EnableSpringDataWebSupport}. + * + * @author Oliver Gierke + * @see DATACMNS-330 + */ +public class EnableSpringDataWebSupportIntegrationTests { + + @Configuration + @EnableSpringDataWebSupport + static class SampleConfig { + + } + + @After + public void tearDown() { + reEnableHateoas(); + } + + @Test + public void registersBasicBeanDefinitions() throws Exception { + + ApplicationContext context = WebTestUtils.createApplicationContext(SampleConfig.class); + List names = Arrays.asList(context.getBeanDefinitionNames()); + + assertThat(names, hasItems("pageableResolver", "sortResolver", "mvcDomainClassConverter")); + + assertResolversRegistered(context, SortHandlerMethodArgumentResolver.class, + PageableHandlerMethodArgumentResolver.class); + } + + @Test + public void registersHateoasSpecificBeanDefinitions() throws Exception { + + ApplicationContext context = WebTestUtils.createApplicationContext(SampleConfig.class); + List names = Arrays.asList(context.getBeanDefinitionNames()); + + assertThat(names, hasItems("pagedResourcesAssembler", "pagedResourcesAssemblerArgumentResolver")); + assertResolversRegistered(context, PagedResourcesAssemblerArgumentResolver.class); + } + + @Test + public void doesNotRegisterHateoasSpecificComponentsIfHateoasNotPresent() throws Exception { + + hideHateoas(); + + ApplicationContext context = WebTestUtils.createApplicationContext(SampleConfig.class); + List names = Arrays.asList(context.getBeanDefinitionNames()); + + assertThat(names, not(hasItems("pagedResourcesAssembler", "pagedResourcesAssemblerArgumentResolver"))); + } + + @SuppressWarnings("unchecked") + private static void assertResolversRegistered(ApplicationContext context, Class... resolverTypes) { + + RequestMappingHandlerAdapter adapter = context.getBean(RequestMappingHandlerAdapter.class); + assertThat(adapter, is(notNullValue())); + List resolvers = adapter.getCustomArgumentResolvers(); + + List> resolverMatchers = new ArrayList>(resolverTypes.length); + + for (Class resolverType : resolverTypes) { + resolverMatchers.add(instanceOf(resolverType)); + } + + assertThat(resolvers, hasItems(resolverMatchers.toArray(new Matcher[resolverMatchers.size()]))); + } + + private static void hideHateoas() throws Exception { + + Field field = ReflectionUtils.findField(SpringDataWebConfigurationImportSelector.class, "HATEOAS_PRESENT"); + ReflectionUtils.makeAccessible(field); + ReflectionUtils.setField(field, null, false); + } + + private static void reEnableHateoas() { + + Field field = ReflectionUtils.findField(SpringDataWebConfigurationImportSelector.class, "HATEOAS_PRESENT"); + ReflectionUtils.makeAccessible(field); + ReflectionUtils.setField(field, null, true); + } +} diff --git a/src/test/java/org/springframework/data/web/config/PageableResourcesAssemblerIntegrationTests.java b/src/test/java/org/springframework/data/web/config/PageableResourcesAssemblerIntegrationTests.java new file mode 100644 index 000000000..ee4d7bde9 --- /dev/null +++ b/src/test/java/org/springframework/data/web/config/PageableResourcesAssemblerIntegrationTests.java @@ -0,0 +1,96 @@ +/* + * 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.config; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import java.util.Arrays; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PagedResourcesAssembler; +import org.springframework.data.web.WebTestUtils; +import org.springframework.hateoas.Link; +import org.springframework.hateoas.PagedResources; +import org.springframework.hateoas.Resource; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.context.WebApplicationContext; + +/** + * Integration tests for {@link PagedResourcesAssembler}. + * + * @author Oliver Gierke + */ +public class PageableResourcesAssemblerIntegrationTests { + + @Configuration + @EnableSpringDataWebSupport + static class Config { + + @Bean + public SampleController controller() { + return new SampleController(); + } + } + + @Before + public void setUp() { + WebTestUtils.initWebTest(); + } + + @Test + public void injectsPagedResourcesAssembler() { + + WebApplicationContext context = WebTestUtils.createApplicationContext(Config.class); + SampleController controller = context.getBean(SampleController.class); + + assertThat(controller.assembler, is(notNullValue())); + + PagedResources> resources = controller.sample(new PageRequest(1, 1)); + + assertThat(resources.getLink(Link.REL_PREVIOUS), is(notNullValue())); + assertThat(resources.getLink(Link.REL_NEXT), is(notNullValue())); + } + + @Controller + static class SampleController { + + @Autowired + PagedResourcesAssembler assembler; + + @RequestMapping("/persons") + PagedResources> sample(Pageable pageable) { + + Page page = new PageImpl(Arrays.asList(new Person()), pageable, pageable.getOffset() + + pageable.getPageSize() + 1); + + return assembler.toResource(page); + } + } + + static class Person { + + } +} diff --git a/template.mf b/template.mf index 04d34ea4e..cd1922f52 100644 --- a/template.mf +++ b/template.mf @@ -17,8 +17,7 @@ Import-Template: org.springframework.context.*;version="${spring30:[=.=.=,+1.1.0)}";resolution:=optional, org.springframework.dao.*;version="${spring30:[=.=.=,+1.1.0)}";resolution:=optional, org.springframework.expression.*;version="${spring30:[=.=.=,+1.1.0)}";resolution:=optional, - org.springframework.expression.spel.standard.*;version="${spring30:[=.=.=,+1.1.0)}";resolution:=optional, - org.springframework.expression.spel.support.*;version="${spring30:[=.=.=,+1.1.0)}";resolution:=optional, + org.springframework.format.*;version="${spring30:[=.=.=,+1.1.0)}";resolution:=optional, org.springframework.hateoas.*;version="${springhateoas:[=.=.=,+1.1.0)}";resolution:=optional, org.springframework.oxm.*;version="${spring30:[=.=.=,+1.1.0)}";resolution:=optional, org.springframework.transaction.*;version="${spring30:[=.=.=,+1.1.0)}";resolution:=optional,