DATAREST-467 - RepositoryEntityLinks now exposes methods to create links to search resources.

RepositoryEntityLinks now exposes methods to obtain links to all search resources and individual ones, including overloads to pre-expand Pageable and Sort parameters potentially contained in the method signature.

Search links now also contain a projection template variable if the type returned by the query method backing the search resource has projections registered.
This commit is contained in:
Oliver Gierke
2015-01-27 08:23:50 +01:00
parent 7d9db83e97
commit 14cee9c94f
15 changed files with 719 additions and 116 deletions

View File

@@ -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();
}

View File

@@ -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> 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<ParameterMetadata> 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);
}
}

View File

@@ -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);
}

View File

@@ -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<MethodResourceMapping>,
return mapping == null ? null : mapping.getMethod();
}
/**
* Returns the mappings for all exported query methods.
*
* @return
* @since 2.3
*/
public Iterable<MethodResourceMapping> getExportedMappings() {
Set<MethodResourceMapping> result = new HashSet<MethodResourceMapping>(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()

View File

@@ -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<Person, Long> {
@@ -138,5 +155,7 @@ public class RepositoryMethodResourceMappingUnitTests {
Page<Person> findByEmailAddress(String email, Pageable pageable);
Page<Person> findByEmailAddress(String email, Sort pageable);
int countByLastname(String lastname);
}
}

View File

@@ -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<Object> assembler;
private final HateoasSortHandlerMethodArgumentResolver sortResolver;
@@ -84,7 +77,7 @@ class RepositorySearchController extends AbstractRepositoryRestController {
* @param mappings must not be {@literal null}.
*/
@Autowired
public RepositorySearchController(PagedResourcesAssembler<Object> assembler, EntityLinks entityLinks,
public RepositorySearchController(PagedResourcesAssembler<Object> 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<Link> links = new ArrayList<Link>();
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.
*

View File

@@ -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<Class<?>> SUPPORTED_TYPES = Collections.unmodifiableSet(new HashSet<Class<?>>(Arrays
.<Class<?>> 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());
}
}

View File

@@ -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());
}

View File

@@ -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);
}

View File

@@ -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<BackendIdConverter, Class<?>> 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<BackendIdConverter, Class<?>> 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<Link> links = new ArrayList<Link>();
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<TemplateVariable> variables = new ArrayList<TemplateVariable>();
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();
}
}

View File

@@ -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<Object> response = controller.executeSearch(resourceInformation, getRequest(parameters),

View File

@@ -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());
}
}

View File

@@ -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")).//

View File

@@ -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<Person, Long> {
@RestResource(rel = "firstname", path = "firstname")
Page<Person> findByFirstName(@Param("firstName") String firstName, Pageable pageable);
Page<Person> findByFirstName(@Param("firstname") String firstName, Pageable pageable);
Person findFirstPersonByFirstName(@Param("firstName") String firstName);
@RestResource(rel = "lastname", path = "lastname")
List<Person> findByLastName(@Param("lastname") String lastName, Sort sort);
Person findFirstPersonByFirstName(@Param("firstname") String firstName);
Page<Person> findByCreatedGreaterThan(@Param("date") Date date, Pageable pageable);

View File

@@ -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")));
}
}