diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/MethodResourceMapping.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/MethodResourceMapping.java index 5504ea9d8..5a058a1d9 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/MethodResourceMapping.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/MethodResourceMapping.java @@ -16,6 +16,9 @@ package org.springframework.data.rest.core.mapping; import java.lang.reflect.Method; +import java.util.Collection; +import java.util.Map; +import java.util.Optional; /** * A {@link ResourceMapping} that is backed by a {@link Method}. @@ -44,4 +47,13 @@ public interface MethodResourceMapping extends ResourceMapping { * @return */ boolean isSortableResource(); + + /** + * Returns the domain type that the query method returns. This will inspect wrapper types ({@link Collection}s, + * {@link Map}s, {@link Optional}s etc.) for their elemtn or value types. + * + * @return will never be {@literal null}. + * @since 2.3 + */ + Class getReturnedDomainType(); } diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryMethodResourceMapping.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryMethodResourceMapping.java index 2bf7288c5..1986007b7 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryMethodResourceMapping.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryMethodResourceMapping.java @@ -25,6 +25,7 @@ import org.springframework.core.MethodParameter; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; +import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.query.Param; import org.springframework.data.rest.core.Path; import org.springframework.data.rest.core.annotation.RestResource; @@ -48,6 +49,7 @@ class RepositoryMethodResourceMapping implements MethodResourceMapping { private final Method method; private final boolean paging; private final boolean sorting; + private final RepositoryMetadata metadata; private final List parameterMetadata; @@ -57,7 +59,7 @@ class RepositoryMethodResourceMapping implements MethodResourceMapping { * @param method must not be {@literal null}. * @param resourceMapping must not be {@literal null}. */ - public RepositoryMethodResourceMapping(Method method, ResourceMapping resourceMapping) { + public RepositoryMethodResourceMapping(Method method, ResourceMapping resourceMapping, RepositoryMetadata metadata) { Assert.notNull(method, "Method must not be null!"); Assert.notNull(resourceMapping, "ResourceMapping must not be null!"); @@ -76,6 +78,7 @@ class RepositoryMethodResourceMapping implements MethodResourceMapping { this.paging = parameterTypes.contains(Pageable.class); this.sorting = parameterTypes.contains(Sort.class); + this.metadata = metadata; } private static final List discoverParameterMetadata(Method method, String baseRel) { @@ -162,4 +165,13 @@ class RepositoryMethodResourceMapping implements MethodResourceMapping { public ResourceDescription getDescription() { return null; } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.mapping.MethodResourceMapping#getProjectionSourceType() + */ + @Override + public Class getReturnedDomainType() { + return metadata.getReturnedDomainClass(method); + } } diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryResourceMappings.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryResourceMappings.java index 90b9c7fd8..c1d156e66 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryResourceMappings.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryResourceMappings.java @@ -132,7 +132,7 @@ public class RepositoryResourceMappings implements ResourceMappings { if (resourceMapping.isExported()) { for (Method queryMethod : repositoryInformation.getQueryMethods()) { RepositoryMethodResourceMapping methodMapping = new RepositoryMethodResourceMapping(queryMethod, - resourceMapping); + resourceMapping, repositoryInformation); if (methodMapping.isExported()) { mappings.add(methodMapping); } diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/SearchResourceMappings.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/SearchResourceMappings.java index 9497c02fb..046c709f9 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/SearchResourceMappings.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/SearchResourceMappings.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2014 the original author or authors. + * 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. @@ -17,9 +17,11 @@ package org.springframework.data.rest.core.mapping; import java.lang.reflect.Method; import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Set; import org.springframework.data.rest.core.Path; import org.springframework.util.Assert; @@ -77,6 +79,46 @@ public class SearchResourceMappings implements Iterable, return mapping == null ? null : mapping.getMethod(); } + /** + * Returns the mappings for all exported query methods. + * + * @return + * @since 2.3 + */ + public Iterable getExportedMappings() { + + Set result = new HashSet(mappings.values().size()); + + for (MethodResourceMapping mapping : this) { + if (mapping.isExported()) { + result.add(mapping); + } + } + + return result; + } + + /** + * Returns the {@link MappingResourceMetadata} for the given relation name. + * + * @param rel must not be {@literal null} or empty. + * @return + * @since 2.3 + */ + public MethodResourceMapping getExportedMethodMappingForRel(String rel) { + + Assert.hasText(rel, "Rel must not be null or empty!"); + + for (MethodResourceMapping mapping : this) { + + if (mapping.isExported() && mapping.getRel().endsWith(rel)) { + return mapping; + } + } + + return null; + } + /* * (non-Javadoc) * @see org.springframework.data.rest.core.mapping.ResourceMapping#getPath() diff --git a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/RepositoryMethodResourceMappingUnitTests.java b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/RepositoryMethodResourceMappingUnitTests.java index 407d99662..e3f766004 100644 --- a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/RepositoryMethodResourceMappingUnitTests.java +++ b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/RepositoryMethodResourceMappingUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2014 the original author or authors. + * 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. @@ -45,7 +45,7 @@ public class RepositoryMethodResourceMappingUnitTests { public void defaultsMappingToMethodName() throws Exception { Method method = PersonRepository.class.getMethod("findByLastname", String.class); - ResourceMapping mapping = new RepositoryMethodResourceMapping(method, resourceMapping); + ResourceMapping mapping = getMappingFor(method); assertThat(mapping.getPath(), is(new Path("findByLastname"))); } @@ -54,7 +54,7 @@ public class RepositoryMethodResourceMappingUnitTests { public void usesConfiguredNameWithLeadingSlash() throws Exception { Method method = PersonRepository.class.getMethod("findByFirstname", String.class); - ResourceMapping mapping = new RepositoryMethodResourceMapping(method, resourceMapping); + ResourceMapping mapping = getMappingFor(method); assertThat(mapping.getPath(), is(new Path("bar"))); } @@ -66,7 +66,7 @@ public class RepositoryMethodResourceMappingUnitTests { public void doesNotDiscoverAnyParametersIfNotAnnotated() throws Exception { Method method = PersonRepository.class.getMethod("findByLastname", String.class); - MethodResourceMapping mapping = new RepositoryMethodResourceMapping(method, resourceMapping); + MethodResourceMapping mapping = getMappingFor(method); assertThat(mapping.getParametersMetadata().getParameterNames(), is(emptyIterable())); } @@ -78,7 +78,7 @@ public class RepositoryMethodResourceMappingUnitTests { public void resolvesParameterNamesIfNotAnnotated() throws Exception { Method method = PersonRepository.class.getMethod("findByFirstname", String.class); - MethodResourceMapping mapping = new RepositoryMethodResourceMapping(method, resourceMapping); + MethodResourceMapping mapping = getMappingFor(method); assertThat(mapping.getParametersMetadata().getParameterNames(), hasSize(1)); assertThat(mapping.getParametersMetadata().getParameterNames(), hasItem("firstname")); @@ -91,7 +91,7 @@ public class RepositoryMethodResourceMappingUnitTests { public void considersPagingFinderAPagingResource() throws Exception { Method method = PersonRepository.class.getMethod("findByEmailAddress", String.class, Pageable.class); - MethodResourceMapping mapping = new RepositoryMethodResourceMapping(method, resourceMapping); + MethodResourceMapping mapping = getMappingFor(method); assertThat(mapping.isPagingResource(), is(true)); } @@ -100,7 +100,7 @@ public class RepositoryMethodResourceMappingUnitTests { public void usesMethodNameAsRelFallbackEvenIfPathIsConfigured() throws Exception { Method method = PersonRepository.class.getMethod("findByEmailAddress", String.class, Pageable.class); - MethodResourceMapping mapping = new RepositoryMethodResourceMapping(method, resourceMapping); + MethodResourceMapping mapping = getMappingFor(method); assertThat(mapping.getRel(), is("findByEmailAddress")); } @@ -112,16 +112,33 @@ public class RepositoryMethodResourceMappingUnitTests { public void considersResourceSortableIfSortParameterIsPresent() throws Exception { Method method = PersonRepository.class.getMethod("findByEmailAddress", String.class, Sort.class); - RepositoryMethodResourceMapping mapping = new RepositoryMethodResourceMapping(method, resourceMapping); + RepositoryMethodResourceMapping mapping = getMappingFor(method); assertThat(mapping.isSortableResource(), is(true)); method = PersonRepository.class.getMethod("findByEmailAddress", String.class, Pageable.class); - mapping = new RepositoryMethodResourceMapping(method, resourceMapping); + mapping = getMappingFor(method); assertThat(mapping.isSortableResource(), is(false)); } + /** + * @see DATAREST-467 + */ + @Test + @SuppressWarnings("rawtypes") + public void returnsDomainTypeAsProjectionSourceType() throws Exception { + + Method method = PersonRepository.class.getMethod("findByLastname", String.class); + MethodResourceMapping mapping = getMappingFor(method); + + assertThat(mapping.getReturnedDomainType(), is(equalTo((Class) Person.class))); + } + + private RepositoryMethodResourceMapping getMappingFor(Method method) { + return new RepositoryMethodResourceMapping(method, resourceMapping, metadata); + } + static class Person {} interface PersonRepository extends Repository { @@ -138,5 +155,7 @@ public class RepositoryMethodResourceMappingUnitTests { Page findByEmailAddress(String email, Pageable pageable); Page findByEmailAddress(String email, Sort pageable); + + int countByLastname(String lastname); } } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositorySearchController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositorySearchController.java index b49856a9d..fbc8e14c5 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositorySearchController.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositorySearchController.java @@ -26,24 +26,18 @@ import java.util.Map; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Sort; import org.springframework.data.repository.support.RepositoryInvoker; -import org.springframework.data.rest.core.mapping.MethodResourceMapping; -import org.springframework.data.rest.core.mapping.ParameterMetadata; import org.springframework.data.rest.core.mapping.ResourceMappings; import org.springframework.data.rest.core.mapping.SearchResourceMappings; import org.springframework.data.rest.webmvc.support.DefaultedPageable; +import org.springframework.data.rest.webmvc.support.RepositoryEntityLinks; import org.springframework.data.web.HateoasSortHandlerMethodArgumentResolver; import org.springframework.data.web.PagedResourcesAssembler; import org.springframework.hateoas.EntityLinks; import org.springframework.hateoas.Link; -import org.springframework.hateoas.LinkBuilder; import org.springframework.hateoas.Links; import org.springframework.hateoas.Resource; import org.springframework.hateoas.ResourceSupport; import org.springframework.hateoas.Resources; -import org.springframework.hateoas.TemplateVariable; -import org.springframework.hateoas.TemplateVariable.VariableType; -import org.springframework.hateoas.TemplateVariables; -import org.springframework.hateoas.UriTemplate; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -56,7 +50,6 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.context.request.WebRequest; -import org.springframework.web.util.UriComponentsBuilder; /** * Controller to lookup and execute searches on a given repository. @@ -70,7 +63,7 @@ class RepositorySearchController extends AbstractRepositoryRestController { private static final String SEARCH = "/search"; private static final String BASE_MAPPING = "/{repository}" + SEARCH; - private final EntityLinks entityLinks; + private final RepositoryEntityLinks entityLinks; private final ResourceMappings mappings; private final PagedResourcesAssembler assembler; private final HateoasSortHandlerMethodArgumentResolver sortResolver; @@ -84,7 +77,7 @@ class RepositorySearchController extends AbstractRepositoryRestController { * @param mappings must not be {@literal null}. */ @Autowired - public RepositorySearchController(PagedResourcesAssembler assembler, EntityLinks entityLinks, + public RepositorySearchController(PagedResourcesAssembler assembler, RepositoryEntityLinks entityLinks, ResourceMappings mappings, HateoasSortHandlerMethodArgumentResolver sortResolver) { super(assembler); @@ -144,7 +137,7 @@ class RepositorySearchController extends AbstractRepositoryRestController { verifySearchesExposed(resourceInformation); - Links queryMethodLinks = getSearchLinks(resourceInformation.getDomainType()); + Links queryMethodLinks = entityLinks.linksToSearchResources(resourceInformation.getDomainType()); if (queryMethodLinks.isEmpty()) { throw new ResourceNotFoundException(); @@ -291,50 +284,6 @@ class RepositorySearchController extends AbstractRepositoryRestController { return resultToResources(result, assembler, null); } - /** - * Returns {@link Links} to the individual searches exposed. - * - * @param domainType the domain type we want to obtain the search links for. - * @return - */ - private Links getSearchLinks(Class domainType) { - - List links = new ArrayList(); - - SearchResourceMappings searchMappings = mappings.getSearchResourceMappings(domainType); - LinkBuilder builder = entityLinks.linkFor(domainType).slash(searchMappings.getPath()); - - for (MethodResourceMapping mapping : searchMappings) { - - if (!mapping.isExported()) { - continue; - } - - TemplateVariables variables = new TemplateVariables(); - - for (ParameterMetadata metadata : mapping.getParametersMetadata()) { - variables = variables.concat(new TemplateVariable(metadata.getName(), VariableType.REQUEST_PARAM)); - } - - String href = builder.slash(mapping.getPath()).toString().concat(variables.toString()); - - Link link = new Link(href, mapping.getRel()); - - if (mapping.isPagingResource()) { - link = assembler.appendPaginationParameterTemplates(link); - } else if (mapping.isSortableResource()) { - - TemplateVariables sortVariable = sortResolver.getSortTemplateVariables(null, UriComponentsBuilder - .fromUriString(link.expand().getHref()).build()); - link = new Link(new UriTemplate(link.getHref()).with(sortVariable), link.getRel()); - } - - links.add(link); - } - - return new Links(links); - } - /** * Verifies that the given {@link RootResourceInformation} has searches exposed. * diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/ArgumentResolverPagingAndSortingTemplateVariables.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/ArgumentResolverPagingAndSortingTemplateVariables.java new file mode 100644 index 000000000..a6a1595ec --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/ArgumentResolverPagingAndSortingTemplateVariables.java @@ -0,0 +1,107 @@ +/* + * Copyright 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.rest.webmvc.config; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.springframework.core.MethodParameter; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.rest.webmvc.support.PagingAndSortingTemplateVariables; +import org.springframework.data.web.HateoasPageableHandlerMethodArgumentResolver; +import org.springframework.data.web.HateoasSortHandlerMethodArgumentResolver; +import org.springframework.hateoas.TemplateVariables; +import org.springframework.util.Assert; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; + +/** + * {@link PagingAndSortingTemplateVariables} implementation to delegate to the HATEOAS-enabled + * {@link HandlerMethodArgumentResolver}s for {@link Pageable} and {@link Sort}. + * + * @author Oliver Gierke + * @since 2.3 + */ +class ArgumentResolverPagingAndSortingTemplateVariables implements PagingAndSortingTemplateVariables { + + private static final Set> SUPPORTED_TYPES = Collections.unmodifiableSet(new HashSet>(Arrays + .> asList(Pageable.class, Sort.class))); + + private final HateoasPageableHandlerMethodArgumentResolver pagingResolver; + private final HateoasSortHandlerMethodArgumentResolver sortResolver; + + /** + * Creates a new {@link ArgumentResolverPagingAndSortingTemplateVariables} using the given + * {@link HateoasPageableHandlerMethodArgumentResolver} and {@link HateoasSortHandlerMethodArgumentResolver}. + * + * @param pagingResolver must not be {@literal null}. + * @param sortResolver must not be {@literal null}. + */ + public ArgumentResolverPagingAndSortingTemplateVariables(HateoasPageableHandlerMethodArgumentResolver pagingResolver, + HateoasSortHandlerMethodArgumentResolver sortResolver) { + + Assert.notNull(pagingResolver, "HateoasPageableHandlerMethodArgumentResolver must not be null!"); + Assert.notNull(sortResolver, "HateoasSortHandlerMethodArgumentResolver must not be null!"); + + this.pagingResolver = pagingResolver; + this.sortResolver = sortResolver; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.webmvc.support.PagingAndSortingTemplateVariables#getPaginationTemplateVariables(org.springframework.core.MethodParameter, org.springframework.web.util.UriComponents) + */ + @Override + public TemplateVariables getPaginationTemplateVariables(MethodParameter parameter, UriComponents components) { + return pagingResolver.getPaginationTemplateVariables(parameter, components); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.webmvc.support.PagingAndSortingTemplateVariables#getSortTemplateVariables(org.springframework.core.MethodParameter, org.springframework.web.util.UriComponents) + */ + @Override + public TemplateVariables getSortTemplateVariables(MethodParameter parameter, UriComponents template) { + return sortResolver.getSortTemplateVariables(parameter, template); + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.mvc.UriComponentsContributor#enhance(org.springframework.web.util.UriComponentsBuilder, org.springframework.core.MethodParameter, java.lang.Object) + */ + @Override + public void enhance(UriComponentsBuilder builder, MethodParameter parameter, Object value) { + + if (value instanceof Pageable) { + pagingResolver.enhance(builder, parameter, value); + } else if (value instanceof Sort) { + sortResolver.enhance(builder, parameter, value); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.mvc.UriComponentsContributor#supportsParameter(org.springframework.core.MethodParameter) + */ + @Override + public boolean supportsParameter(MethodParameter parameter) { + return SUPPORTED_TYPES.contains(parameter.getParameterType()); + } +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java index 1a1d6b318..5baa5787f 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java @@ -86,6 +86,7 @@ import org.springframework.data.rest.webmvc.support.DefaultedPageableHandlerMeth import org.springframework.data.rest.webmvc.support.ETagArgumentResolver; import org.springframework.data.rest.webmvc.support.HttpMethodHandlerMethodArgumentResolver; import org.springframework.data.rest.webmvc.support.JpaHelper; +import org.springframework.data.rest.webmvc.support.PagingAndSortingTemplateVariables; import org.springframework.data.rest.webmvc.support.RepositoryEntityLinks; import org.springframework.data.util.AnnotatedTypeScanner; import org.springframework.data.web.HateoasPageableHandlerMethodArgumentResolver; @@ -303,7 +304,11 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon */ @Bean public RepositoryEntityLinks entityLinks() { - return new RepositoryEntityLinks(repositories(), resourceMappings(), config(), pageableResolver(), + + PagingAndSortingTemplateVariables templateVariables = new ArgumentResolverPagingAndSortingTemplateVariables( + pageableResolver(), sortResolver()); + + return new RepositoryEntityLinks(repositories(), resourceMappings(), config(), templateVariables, backendIdConverterRegistry()); } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/PagingAndSortingTemplateVariables.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/PagingAndSortingTemplateVariables.java new file mode 100644 index 000000000..22f4f402e --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/PagingAndSortingTemplateVariables.java @@ -0,0 +1,48 @@ +/* + * Copyright 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.rest.webmvc.support; + +import org.springframework.core.MethodParameter; +import org.springframework.hateoas.TemplateVariables; +import org.springframework.hateoas.mvc.UriComponentsContributor; +import org.springframework.web.util.UriComponents; + +/** + * Interface to abstract the access of {@link TemplateVariables} for pagination and sorting. + * + * @author Oliver Gierke + * @since 2.3 + */ +public interface PagingAndSortingTemplateVariables extends UriComponentsContributor { + + /** + * Returns the {@link TemplateVariables} for pagination. + * + * @param parameter can be {@literal null}. + * @param components must not be {@literal null}. + * @return + */ + TemplateVariables getPaginationTemplateVariables(MethodParameter parameter, UriComponents components); + + /** + * Returns the {@link TemplateVariables} for sorting. + * + * @param parameter can be {@literal null}. + * @param components must not be {@literal null}. + * @return + */ + TemplateVariables getSortTemplateVariables(MethodParameter parameter, UriComponents components); +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinks.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinks.java index 6fbe62c7c..d0fa1a75a 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinks.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinks.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2014 the original author or authors. + * Copyright 2012-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. @@ -18,27 +18,36 @@ package org.springframework.data.rest.webmvc.support; import static org.springframework.hateoas.TemplateVariable.VariableType.*; import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.data.repository.support.Repositories; import org.springframework.data.rest.core.config.ProjectionDefinitionConfiguration; import org.springframework.data.rest.core.config.RepositoryRestConfiguration; +import org.springframework.data.rest.core.mapping.MethodResourceMapping; +import org.springframework.data.rest.core.mapping.ParameterMetadata; +import org.springframework.data.rest.core.mapping.ResourceMapping; import org.springframework.data.rest.core.mapping.ResourceMappings; import org.springframework.data.rest.core.mapping.ResourceMetadata; +import org.springframework.data.rest.core.mapping.SearchResourceMappings; import org.springframework.data.rest.webmvc.BaseUri; import org.springframework.data.rest.webmvc.spi.BackendIdConverter; import org.springframework.data.rest.webmvc.spi.BackendIdConverter.DefaultIdConverter; -import org.springframework.data.web.HateoasPageableHandlerMethodArgumentResolver; import org.springframework.hateoas.EntityLinks; import org.springframework.hateoas.Link; import org.springframework.hateoas.LinkBuilder; +import org.springframework.hateoas.Links; import org.springframework.hateoas.TemplateVariable; +import org.springframework.hateoas.TemplateVariable.VariableType; import org.springframework.hateoas.TemplateVariables; import org.springframework.hateoas.UriTemplate; import org.springframework.hateoas.core.AbstractEntityLinks; import org.springframework.plugin.core.PluginRegistry; import org.springframework.util.Assert; +import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; /** @@ -53,7 +62,7 @@ public class RepositoryEntityLinks extends AbstractEntityLinks { private final Repositories repositories; private final ResourceMappings mappings; private final RepositoryRestConfiguration config; - private final HateoasPageableHandlerMethodArgumentResolver resolver; + private final PagingAndSortingTemplateVariables templateVariables; private final PluginRegistry> idConverters; /** @@ -62,24 +71,24 @@ public class RepositoryEntityLinks extends AbstractEntityLinks { * @param repositories must not be {@literal null}. * @param mappings must not be {@literal null}. * @param config must not be {@literal null}. - * @param resolver must not be {@literal null}. + * @param pagingAndSortingTemplateVariables must not be {@literal null}. * @param idConverters must not be {@literal null}. */ @Autowired public RepositoryEntityLinks(Repositories repositories, ResourceMappings mappings, - RepositoryRestConfiguration config, HateoasPageableHandlerMethodArgumentResolver resolver, + RepositoryRestConfiguration config, PagingAndSortingTemplateVariables templateVariables, PluginRegistry> idConverters) { Assert.notNull(repositories, "Repositories must not be null!"); Assert.notNull(mappings, "ResourceMappings must not be null!"); Assert.notNull(config, "RepositoryRestConfiguration must not be null!"); - Assert.notNull(resolver, "HateoasPageableHandlerMethodArgumentResolver must not be null!"); + Assert.notNull(templateVariables, "PagingAndSortingTemplateVariables must not be null!"); Assert.notNull(idConverters, "Id converter registry must not be null!"); this.repositories = repositories; this.mappings = mappings; this.config = config; - this.resolver = resolver; + this.templateVariables = templateVariables; this.idConverters = idConverters; } @@ -112,33 +121,23 @@ public class RepositoryEntityLinks extends AbstractEntityLinks { return linkFor(type); } + /** + * Returns the link to to the paged colelction resource for the given type, pre-expanding the + * + * @param type must not be {@literal null}. + * @param pageable the pageable to can be {@literal null}. + * @return + */ public Link linkToPagedResource(Class type, Pageable pageable) { ResourceMetadata metadata = mappings.getMappingFor(type); - TemplateVariables variables = new TemplateVariables(); - String href = linkFor(type).withSelfRel().getHref(); + String href = linkFor(type).toString(); + UriComponents components = prepareUri(href, metadata, pageable); - if (metadata.isPagingResource()) { + TemplateVariables variables = getTemplateVariables(components, metadata, pageable).// + concat(getProjectionVariable(type)); - UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(href); - - if (pageable != null) { - resolver.enhance(builder, null, pageable); - } - - href = builder.build().toString(); - - variables = variables.concat(resolver.getPaginationTemplateVariables(null, builder.build())); - } - - ProjectionDefinitionConfiguration projectionConfiguration = config.projectionConfiguration(); - - if (projectionConfiguration.hasProjectionFor(type)) { - variables = variables.concat(new TemplateVariable(projectionConfiguration.getParameterName(), REQUEST_PARAM)); - } - - return variables.asList().isEmpty() ? linkFor(type).withRel(metadata.getRel()) : new Link(new UriTemplate(href, - variables), metadata.getRel()); + return new Link(new UriTemplate(href, variables), metadata.getRel()); } /* @@ -163,16 +162,232 @@ public class RepositoryEntityLinks extends AbstractEntityLinks { String mappedId = idConverters.getPluginFor(type, DefaultIdConverter.INSTANCE).toRequestId((Serializable) id, type); Link link = linkFor(type).slash(mappedId).withRel(metadata.getItemResourceRel()); - ProjectionDefinitionConfiguration projectionConfiguration = config.projectionConfiguration(); + return new Link(new UriTemplate(link.getHref(), getProjectionVariable(type)).toString(), + metadata.getItemResourceRel()); + } - if (!projectionConfiguration.hasProjectionFor(type)) { - return link; + /** + * Returns all links to search resource for the given type. + * + * @param type must not be {@literal null}. + * @return + * @since 2.3 + */ + public Links linksToSearchResources(Class type) { + return linksToSearchResources(type, null, null); + } + + /** + * Returns all link to search resources for the given type, pre-expanded with the given {@link Pageable} if + * applicable. + * + * @param type must not be {@literal null}. + * @param pageable can be {@literal null}. + * @return + * @since 2.3 + */ + public Links linksToSearchResources(Class type, Pageable pageable) { + return linksToSearchResources(type, pageable, null); + } + + /** + * Returns all link to search resources for the given type, pre-expanded with the given {@link Sort} if applicable. + * + * @param type must not be {@literal null}. + * @param sort can be {@literal null}. + * @return + * @since 2.3 + */ + public Links linksToSearchResources(Class type, Sort sort) { + return linksToSearchResources(type, null, sort); + } + + /** + * Creates the link to the search resource with the given rel for a given type. + * + * @param domainType must not be {@literal null}. + * @param rel must not be {@literal null} or empty. + * @return + * @since 2.3 + */ + public Link linkToSearchResource(Class domainType, String rel) { + return getSearchResourceLinkFor(domainType, rel, null, null); + } + + /** + * Creates the link to the search resource with the given rel for a given type. Uses the given {@link Pageable} to + * pre-expand potentially available template variables. + * + * @param domainType must not be {@literal null}. + * @param rel must not be {@literal null} or empty. + * @param pageable can be {@literal null}. + * @return + * @since 2.3 + */ + public Link linkToSearchResource(Class domainType, String rel, Pageable pageable) { + return getSearchResourceLinkFor(domainType, rel, pageable, null); + } + + /** + * Creates the link to the search resource with the given rel for a given type. Uses the given {@link Sort} to + * pre-expand potentially available template variables. + * + * @param domainType must not be {@literal null}. + * @param rel must not be {@literal null} or empty. + * @param sort can be {@literal null}. + * @return + * @since 2.3 + */ + public Link linkToSearchResource(Class domainType, String rel, Sort sort) { + return getSearchResourceLinkFor(domainType, rel, null, sort); + } + + /** + * Returns all links to search resources of the given type. Pre-expands the template with the given {@link Pageable} + * and {@link Sort} if applicable. + * + * @param type must not be {@literal null}. + * @param pageable can be {@literal null}. + * @param sort can be {@literal null}. + * @return + */ + private Links linksToSearchResources(Class type, Pageable pageable, Sort sort) { + + List links = new ArrayList(); + + SearchResourceMappings searchMappings = mappings.getSearchResourceMappings(type); + + for (MethodResourceMapping mapping : searchMappings.getExportedMappings()) { + links.add(getSearchResourceLinkFor(type, mapping.getRel(), pageable, sort)); } - String parameterName = projectionConfiguration.getParameterName(); - TemplateVariables templateVariables = new TemplateVariables(new TemplateVariable(parameterName, REQUEST_PARAM)); - UriTemplate template = new UriTemplate(link.getHref(), templateVariables); + return new Links(links); + } - return new Link(template.toString(), metadata.getItemResourceRel()); + /** + * Returns the link pointing to the search resource with the given rel of the given type and pre-expands the + * calculated URi tempalte with the given {@link Pageable} and {@link Sort}. + * + * @param type must not be {@literal null}. + * @param rel must not be {@literal null} or empty. + * @param pageable can be {@literal null}. + * @param sort can be {@literal null}. + * @return + */ + private Link getSearchResourceLinkFor(Class type, String rel, Pageable pageable, Sort sort) { + + Assert.notNull(type, "Domain type must not be null!"); + Assert.hasText(rel, "Relation name must not be null or empty!"); + + SearchResourceMappings searchMappings = mappings.getSearchResourceMappings(type); + MethodResourceMapping mapping = searchMappings.getExportedMethodMappingForRel(rel); + + if (mapping == null) { + return null; + } + + LinkBuilder builder = linkFor(type).slash(mappings.getSearchResourceMappings(type).getPath()); + UriComponents uriComponents = prepareUri(builder.toString(), mapping, pageable, sort); + + TemplateVariables variables = getParameterVariables(mapping).// + concat(getTemplateVariables(uriComponents, mapping, pageable, sort)).// + concat(getProjectionVariable(mapping.getReturnedDomainType())); + + return new Link(new UriTemplate(builder.slash(mapping.getPath()).toString(), variables), mapping.getRel()); + } + + /** + * Returns the {@link TemplateVariables} to be added for pagination for the given {@link UriComponentsBuilder} in case + * the given {@link ResourceMapping} is a paging resource. + * + * @param components must not be {@literal null}. + * @param mapping must not be {@literal null}. + * @param pageable can be {@literal null}. + * @return will never be {@literal null}. + */ + private TemplateVariables getTemplateVariables(UriComponents components, ResourceMapping mapping, Pageable pageable) { + + if (mapping.isPagingResource()) { + return templateVariables.getPaginationTemplateVariables(null, components); + } else { + return TemplateVariables.NONE; + } + } + + /** + * Returns all {@link TemplateVariables} that need to be added based on the given {@link UriComponents}, + * {@link MethodResourceMapping}, {@link Pageable} and {@link Sort}. + * + * @param components must not be {@literal null}. + * @param mapping must not be {@literal null}. + * @param pageable can be {@literal null} + * @param sort can be {@literal null}. + * @return will never be {@literal null}. + */ + private TemplateVariables getTemplateVariables(UriComponents components, MethodResourceMapping mapping, + Pageable pageable, Sort sort) { + + if (mapping.isSortableResource()) { + return templateVariables.getSortTemplateVariables(null, components); + } else { + return getTemplateVariables(components, mapping, pageable); + } + } + + /** + * Returns the {@link TemplateVariables} for the projection parameter if projections are vonfigured for the given + * type. + * + * @param type must not be {@literal null}. + * @return will never be {@literal null}. + */ + private TemplateVariables getProjectionVariable(Class type) { + + ProjectionDefinitionConfiguration projectionConfiguration = config.projectionConfiguration(); + + if (projectionConfiguration.hasProjectionFor(type)) { + return new TemplateVariables(new TemplateVariable(projectionConfiguration.getParameterName(), REQUEST_PARAM)); + } else { + return TemplateVariables.NONE; + } + } + + /** + * Returns the {@link TemplateVariables} for all parameters of the given {@link MethodResourceMapping}. + * + * @param mapping must not be {@literal null}. + * @return will never be {@literal null}. + */ + private TemplateVariables getParameterVariables(MethodResourceMapping mapping) { + + List variables = new ArrayList(); + + for (ParameterMetadata metadata : mapping.getParametersMetadata()) { + variables.add(new TemplateVariable(metadata.getName(), VariableType.REQUEST_PARAM)); + } + + return new TemplateVariables(variables); + } + + private UriComponents prepareUri(String uri, MethodResourceMapping mapping, Pageable pageable, Sort sort) { + + if (mapping.isSortableResource()) { + UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString(uri); + templateVariables.enhance(uriBuilder, null, sort); + return uriBuilder.build(); + } else { + return prepareUri(uri, mapping, pageable); + } + } + + private UriComponents prepareUri(String uri, ResourceMapping mapping, Pageable pageable) { + + UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString(uri); + + if (mapping.isPagingResource()) { + templateVariables.enhance(uriBuilder, null, pageable); + } + + return uriBuilder.build(); } } diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositorySearchControllerIntegrationTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositorySearchControllerIntegrationTests.java index 8e8f3a971..2c182c675 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositorySearchControllerIntegrationTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositorySearchControllerIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2014 the original author or authors. + * 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. @@ -65,12 +65,14 @@ public class RepositorySearchControllerIntegrationTests extends AbstractControll ResourceSupport resource = controller.listSearches(request); ResourceTester tester = ResourceTester.of(resource); - tester.assertNumberOfLinks(4); - tester.assertHasLinkEndingWith("findFirstPersonByFirstName", "findFirstPersonByFirstName{?firstName}"); - tester.assertHasLinkEndingWith("firstname", "firstname{?firstName,page,size,sort}"); + tester.assertNumberOfLinks(5); + tester.assertHasLinkEndingWith("findFirstPersonByFirstName", "findFirstPersonByFirstName{?firstname,projection}"); + tester.assertHasLinkEndingWith("firstname", "firstname{?firstname,page,size,sort,projection}"); + tester.assertHasLinkEndingWith("lastname", "lastname{?lastname,sort,projection}"); tester.assertHasLinkEndingWith("findByCreatedUsingISO8601Date", - "findByCreatedUsingISO8601Date{?date,page,size,sort}"); - tester.assertHasLinkEndingWith("findByCreatedGreaterThan", "findByCreatedGreaterThan{?date,page,size,sort}"); + "findByCreatedUsingISO8601Date{?date,page,size,sort,projection}"); + tester.assertHasLinkEndingWith("findByCreatedGreaterThan", + "findByCreatedGreaterThan{?date,page,size,sort,projection}"); } @Test(expected = ResourceNotFoundException.class) @@ -86,7 +88,7 @@ public class RepositorySearchControllerIntegrationTests extends AbstractControll @Test public void executesSearchAgainstRepository() { - RequestParameters parameters = new RequestParameters("firstName", "John"); + RequestParameters parameters = new RequestParameters("firstname", "John"); RootResourceInformation resourceInformation = getResourceInformation(Person.class); ResponseEntity response = controller.executeSearch(resourceInformation, getRequest(parameters), diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/config/ArgumentResolverPagingAndSortingTemplateVariablesUnitTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/config/ArgumentResolverPagingAndSortingTemplateVariablesUnitTests.java new file mode 100644 index 000000000..630a3d919 --- /dev/null +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/config/ArgumentResolverPagingAndSortingTemplateVariablesUnitTests.java @@ -0,0 +1,117 @@ +/* + * Copyright 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.rest.webmvc.config; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.*; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.core.MethodParameter; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.rest.webmvc.support.PagingAndSortingTemplateVariables; +import org.springframework.data.web.HateoasPageableHandlerMethodArgumentResolver; +import org.springframework.data.web.HateoasSortHandlerMethodArgumentResolver; +import org.springframework.hateoas.mvc.UriComponentsContributor; +import org.springframework.web.util.UriComponentsBuilder; + +/** + * Unit tests for {@link ArgumentResolverPagingAndSortingTemplateVariables}. + * + * @author Oliver Gierke + */ +@RunWith(MockitoJUnitRunner.class) +public class ArgumentResolverPagingAndSortingTemplateVariablesUnitTests { + + @Mock HateoasPageableHandlerMethodArgumentResolver pageableResolver; + @Mock HateoasSortHandlerMethodArgumentResolver sortResolver; + @Mock UriComponentsBuilder builder; + + /** + * @see DATAREST-467 + */ + @Test(expected = IllegalArgumentException.class) + public void rejectsNullArgumentResolverForPageable() { + new ArgumentResolverPagingAndSortingTemplateVariables(null, sortResolver); + } + + /** + * @see DATAREST-467 + */ + @Test(expected = IllegalArgumentException.class) + public void rejectsNullArgumentResolverForSort() { + new ArgumentResolverPagingAndSortingTemplateVariables(pageableResolver, null); + } + + /** + * @see DATAREST-467 + */ + @Test + public void supportsPageableAndSortMethodParameters() { + + PagingAndSortingTemplateVariables variables = new ArgumentResolverPagingAndSortingTemplateVariables( + pageableResolver, sortResolver); + + assertThat(variables.supportsParameter(getParameterMock(Pageable.class)), is(true)); + assertThat(variables.supportsParameter(getParameterMock(Sort.class)), is(true)); + assertThat(variables.supportsParameter(getParameterMock(Object.class)), is(false)); + } + + /** + * @see DATAREST-467 + */ + @Test + public void forwardsEnhanceRequestForPageable() { + assertForwardsEnhanceFor(new PageRequest(0, 10), pageableResolver, sortResolver); + } + + /** + * @see DATAREST-467 + */ + @Test + public void forwardsEnhanceRequestForSort() { + assertForwardsEnhanceFor(new Sort("property"), sortResolver, pageableResolver); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private static MethodParameter getParameterMock(Class type) { + + MethodParameter parameter = mock(MethodParameter.class); + when(parameter.getParameterType()).thenReturn((Class) type); + + return parameter; + } + + private void assertForwardsEnhanceFor(Object value, UriComponentsContributor expected, + UriComponentsContributor unexpected) { + + PagingAndSortingTemplateVariables variables = new ArgumentResolverPagingAndSortingTemplateVariables( + pageableResolver, sortResolver); + + variables.enhance(builder, null, value); + + verify(expected, times(1)).enhance(builder, null, value); + verify(unexpected, times(0)).enhance(Mockito.any(UriComponentsBuilder.class), Mockito.any(MethodParameter.class), + anyObject()); + } +} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaWebTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaWebTests.java index 7abf036ee..91fd8910e 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaWebTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaWebTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2014 the original author or authors. + * 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. @@ -570,7 +570,7 @@ public class JpaWebTests extends CommonWebTests { // Assert sort options advertised assertThat(findBySortedLink.isTemplated(), is(true)); - assertThat(findBySortedLink.getVariableNames(), contains("sort")); + assertThat(findBySortedLink.getVariableNames(), hasItems("sort", "projection")); // Assert results returned as specified client.follow(findBySortedLink.expand("title,desc")).// diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/PersonRepository.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/PersonRepository.java index 49537bbc0..bc22c6090 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/PersonRepository.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/PersonRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2014 the original author or authors. + * 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. @@ -16,9 +16,11 @@ package org.springframework.data.rest.webmvc.jpa; import java.util.Date; +import java.util.List; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.data.repository.query.Param; @@ -37,9 +39,12 @@ import org.springframework.format.annotation.DateTimeFormat.ISO; public interface PersonRepository extends PagingAndSortingRepository { @RestResource(rel = "firstname", path = "firstname") - Page findByFirstName(@Param("firstName") String firstName, Pageable pageable); + Page findByFirstName(@Param("firstname") String firstName, Pageable pageable); - Person findFirstPersonByFirstName(@Param("firstName") String firstName); + @RestResource(rel = "lastname", path = "lastname") + List findByLastName(@Param("lastname") String lastName, Sort sort); + + Person findFirstPersonByFirstName(@Param("firstname") String firstName); Page findByCreatedGreaterThan(@Param("date") Date date, Pageable pageable); diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinksIntegrationTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinksIntegrationTests.java index 587a599a6..eb800e401 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinksIntegrationTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinksIntegrationTests.java @@ -21,6 +21,7 @@ import static org.junit.Assert.*; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; import org.springframework.data.rest.core.config.RepositoryRestConfiguration; import org.springframework.data.rest.webmvc.AbstractControllerIntegrationTests; import org.springframework.data.rest.webmvc.jpa.Book; @@ -28,6 +29,7 @@ import org.springframework.data.rest.webmvc.jpa.JpaRepositoryConfig; import org.springframework.data.rest.webmvc.jpa.Order; import org.springframework.data.rest.webmvc.jpa.Person; import org.springframework.hateoas.Link; +import org.springframework.hateoas.Links; import org.springframework.test.context.ContextConfiguration; /** @@ -94,4 +96,72 @@ public class RepositoryEntityLinksIntegrationTests extends AbstractControllerInt assertThat(link.getVariableNames(), hasSize(2)); assertThat(link.getVariableNames(), hasItems("sort", "projection")); } + + /** + * @see DATAREST-467 + */ + @Test + public void returnsLinksToSearchResources() { + + Links links = entityLinks.linksToSearchResources(Person.class); + + assertThat(links.hasLink("firstname"), is(true)); + + Link firstnameLink = links.getLink("firstname"); + assertThat(firstnameLink.isTemplated(), is(true)); + assertThat(firstnameLink.getVariableNames(), hasItems("page", "size")); + } + + /** + * @see DATAREST-467 + */ + @Test + public void returnsLinkToSearchResource() { + + Link link = entityLinks.linkToSearchResource(Person.class, "firstname"); + + assertThat(link, is(notNullValue())); + assertThat(link.isTemplated(), is(true)); + assertThat(link.getVariableNames(), hasItems("firstname", "page", "size")); + } + + /** + * @see DATAREST-467 + */ + @Test + public void prepopulatesPaginationInformationForSearchResourceLink() { + + Link link = entityLinks.linkToSearchResource(Person.class, "firstname", new PageRequest(0, 10)); + + assertThat(link, is(notNullValue())); + assertThat(link.isTemplated(), is(true)); + assertThat(link.getVariableNames(), hasItem("firstname")); + assertThat(link.getVariableNames(), not(hasItems("page", "size"))); + } + + /** + * @see DATAREST-467 + */ + @Test + public void returnsTemplatedLinkForSortedSearchResource() { + + Link link = entityLinks.linkToSearchResource(Person.class, "lastname"); + + assertThat(link.isTemplated(), is(true)); + assertThat(link.getVariableNames(), hasItems("lastname", "sort")); + } + + /** + * @see DATAREST-467 + */ + @Test + public void prepopulatesSortInformationForSearchResourceLink() { + + Link link = entityLinks.linkToSearchResource(Person.class, "lastname", new Sort("firstname")); + + assertThat(link, is(notNullValue())); + assertThat(link.isTemplated(), is(true)); + assertThat(link.getVariableNames(), hasItem("lastname")); + assertThat(link.getVariableNames(), not(hasItems("sort"))); + } }