DATAREST-229 - RepositoryEntityLinks now exposes templated links.

If the resource exposed for a domain or repository type is considered a paging resource we now return a templated URI. Introduced ResourceMapping.isPagingResource() to allow clients to find out about whether the resource is actually capable of pagination. Adapted implementations to inspect the findAll(…) method as well as the search methods for a Pageable parameter.

Tweaked pom.xml to create correct classpaths if the IDE uses direct workspace project references.
This commit is contained in:
Oliver Gierke
2014-01-22 17:54:35 +01:00
parent 5778ff9940
commit 88b2f05edf
21 changed files with 309 additions and 313 deletions

14
pom.xml
View File

@@ -72,13 +72,6 @@
</dependency>
<!-- Test dependencies -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>${springdata.jpa}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hibernate.javax.persistence</groupId>
@@ -94,6 +87,13 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>${springdata.jpa}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb</artifactId>

View File

@@ -28,17 +28,17 @@
<artifactId>spring-tx</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
<version>${springdata.commons}</version>
</dependency>
<dependency>
<groupId>org.springframework.hateoas</groupId>
<artifactId>spring-hateoas</artifactId>
<version>${spring.hateoas}</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
<version>${springdata.commons}</version>
</dependency>
<dependency>
<groupId>org.springframework.plugin</groupId>

View File

@@ -1,114 +0,0 @@
package org.springframework.data.rest.core.invoke;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.springframework.core.MethodParameter;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.repository.query.Param;
import org.springframework.data.rest.core.support.Methods;
/**
* An abstraction to encapsulate metadata about a repository method.
*
* @author Jon Brisbin
*/
public class RepositoryMethod {
private Method method;
private List<MethodParameter> methodParameters = new ArrayList<MethodParameter>();
private List<String> paramNames = new ArrayList<String>();
private boolean pageable = false;
private boolean sortable = false;
public RepositoryMethod(Method method) {
this.method = method;
Class<?>[] paramTypes = method.getParameterTypes();
String[] paramNames = Methods.NAME_DISCOVERER.getParameterNames(method);
if (null == paramNames) {
paramNames = new String[paramTypes.length];
}
Annotation[][] paramAnnos = method.getParameterAnnotations();
for (int i = 0; i < paramAnnos.length; i++) {
if (paramAnnos[i].length > 0) {
for (Annotation anno : paramAnnos[i]) {
if (Param.class.isAssignableFrom(anno.getClass())) {
Param p = (Param) anno;
paramNames[i] = p.value();
break;
}
}
}
if (null == paramNames[i]) {
paramNames[i] = "arg" + i;
}
}
int idx = 0;
for (Class<?> type : paramTypes) {
if (Pageable.class.isAssignableFrom(type)) {
pageable = true;
}
if (Sort.class.isAssignableFrom(type)) {
sortable = true;
}
methodParameters.add(new MethodParameter(method, idx));
idx++;
}
Collections.addAll(this.paramNames, paramNames);
}
/**
* Get the method parameter types.
*
* @return Array of parameter types.
*/
public List<MethodParameter> getParameters() {
return methodParameters;
}
/**
* Get the method parameter names.
*
* @return Array of parameter names.
*/
public List<String> getParameterNames() {
return paramNames;
}
/**
* Get the reflected {@link Method} to invoke.
*
* @return The {@link Method} to invoke.
*/
public Method getMethod() {
return method;
}
/**
* Flag denoting whether this repository method returns a {@link org.springframework.data.domain.Page} result or not.
*
* @return {@literal true} if this method returns a {@link org.springframework.data.domain.Page}, {@literal false}
* otherwise.
*/
public boolean isPageable() {
return pageable;
}
/**
* Flag denoting whether this repository method accepts sorting information.
*
* @return {@literal true} if this method accepts a {@link Sort}, {@literal false} otherwise.
*/
public boolean isSortable() {
return sortable;
}
}

View File

@@ -1,79 +0,0 @@
package org.springframework.data.rest.core.invoke;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.repository.query.Param;
import org.springframework.util.Assert;
/**
* Represents a query method on a repository interface.
*
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public class RepositoryQueryMethod {
private Method method;
private Class<?>[] paramTypes;
private String[] paramNames;
public RepositoryQueryMethod(Method method) {
this.method = method;
paramTypes = method.getParameterTypes();
paramNames = new String[paramTypes.length];
if (null == paramNames) {
paramNames = new String[paramTypes.length];
}
Annotation[][] paramAnnos = method.getParameterAnnotations();
for (int i = 0; i < paramAnnos.length; i++) {
if (paramAnnos[i].length == 0) {
continue;
}
for (Annotation anno : paramAnnos[i]) {
if (Param.class.isAssignableFrom(anno.getClass())) {
Param p = (Param) anno;
paramNames[i] = p.value();
break;
}
}
if (Pageable.class.isAssignableFrom(paramTypes[i]) || Sort.class.isAssignableFrom(paramTypes[i])) {
continue;
}
Assert.notNull(paramNames[i], "No @Param('name') was provided for parameter " + (i + 1) + " of type "
+ paramTypes[i] + " on " + (method.getDeclaringClass().getName() + "." + method.getName()));
}
}
/**
* The method's parameter types.
*
* @return
*/
public Class<?>[] paramTypes() {
return paramTypes;
}
/**
* The parameter names as pulled from the {@link Param} annotations.
*
* @return
*/
public String[] paramNames() {
return paramNames;
}
/**
* The {@link Method} to invoke.
*
* @return
*/
public Method method() {
return method;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013 the original author or authors.
* Copyright 2013-2014 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,12 +18,14 @@ package org.springframework.data.rest.core.mapping;
import org.springframework.context.annotation.Primary;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.core.Path;
import org.springframework.util.Assert;
/**
* {@link ResourceMetadata} for a single repository.
*
* @author Oliver Gierke
*/
class RepositoryAwareResourceInformation implements ResourceMetadata {
@@ -31,26 +33,37 @@ class RepositoryAwareResourceInformation implements ResourceMetadata {
private final Repositories repositories;
private final CollectionResourceMapping mapping;
private final ResourceMappings provider;
private final RepositoryInformation repositoryInterface;
private final RepositoryMetadata repositoryInterface;
/**
* Creates a new {@link RepositoryAwareResourceInformation} for the given {@link Repositories},
* {@link CollectionResourceMapping}, {@link ResourceMappings} and {@link RepositoryMetadata}.
*
* @param repositories must not be {@literal null}.
* @param mapping must not be {@literal null}.
* @param provider must not be {@literal null}.
* @param repositoryMetadata must not be {@literal null}.
*/
public RepositoryAwareResourceInformation(Repositories repositories, CollectionResourceMapping mapping,
ResourceMappings provider, RepositoryInformation repositoryInterface) {
ResourceMappings provider, RepositoryMetadata repositoryMetadata) {
Assert.notNull(repositories, "Repositories must not be null!");
Assert.notNull(mapping, "ResourceMapping must not be null!");
Assert.notNull(mapping, "CollectionResourceMapping must not be null!");
Assert.notNull(provider, "ResourceMetadataProvider must not be null!");
Assert.notNull(repositoryMetadata, "RepositoryMetadata must not be null!");
this.repositories = repositories;
this.mapping = mapping;
this.provider = provider;
this.repositoryInterface = repositoryInterface;
this.repositoryInterface = repositoryMetadata;
}
/**
* Returns whether the current {@link ResourceMetadata} instance for the repository is the primary one to be used.
* Reflects to the primary state of the bean definition.
*
* @return
*/
public boolean isPrimary() {
return AnnotationUtils.findAnnotation(repositoryInterface.getRepositoryInterface(), Primary.class) != null;
}
@@ -129,6 +142,15 @@ class RepositoryAwareResourceInformation implements ResourceMetadata {
return mapping.getPath();
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.core.mapping.CollectionResourceMapping#isPagingResource()
*/
@Override
public boolean isPagingResource() {
return mapping.isPagingResource();
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.core.mapping.ResourceMetadata#getSearchResourceMappings()

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013 the original author or authors.
* Copyright 2013-2014 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,6 +18,7 @@ package org.springframework.data.rest.core.mapping;
import java.lang.reflect.Modifier;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.rest.core.Path;
import org.springframework.data.rest.core.annotation.RestResource;
import org.springframework.data.rest.core.support.RepositoriesUtils;
@@ -41,9 +42,10 @@ class RepositoryCollectionResourceMapping implements CollectionResourceMapping {
private final RestResource annotation;
private final CollectionResourceMapping domainTypeMapping;
private final boolean repositoryIsExportCandidate;
private final RepositoryMetadata metadata;
public RepositoryCollectionResourceMapping(Class<?> repositoryType) {
this(repositoryType, new EvoInflectorRelProvider());
public RepositoryCollectionResourceMapping(RepositoryMetadata metadata) {
this(metadata, new EvoInflectorRelProvider());
}
/**
@@ -53,7 +55,12 @@ class RepositoryCollectionResourceMapping implements CollectionResourceMapping {
* @param repositoryType must not be {@literal null}.
* @param relProvider must not be {@literal null}.
*/
public RepositoryCollectionResourceMapping(Class<?> repositoryType, RelProvider relProvider) {
public RepositoryCollectionResourceMapping(RepositoryMetadata metadata, RelProvider relProvider) {
Assert.notNull(metadata, "Repository metadata must not be null!");
this.metadata = metadata;
Class<?> repositoryType = metadata.getRepositoryInterface();
Assert.isTrue(RepositoriesUtils.isRepositoryInterface(repositoryType), "Given type is not a repository!");
Assert.notNull(relProvider, "RelProvider must not be null!");
@@ -103,4 +110,13 @@ class RepositoryCollectionResourceMapping implements CollectionResourceMapping {
public Boolean isExported() {
return annotation == null ? repositoryIsExportCandidate && domainTypeMapping.isExported() : annotation.exported();
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.core.mapping.CollectionResourceMapping#isPagingResource()
*/
@Override
public boolean isPagingResource() {
return metadata.isPagingRepository();
}
}

View File

@@ -17,11 +17,13 @@ package org.springframework.data.rest.core.mapping;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.domain.Pageable;
import org.springframework.data.repository.query.Param;
import org.springframework.data.rest.core.Path;
import org.springframework.data.rest.core.annotation.RestResource;
@@ -43,6 +45,7 @@ class RepositoryMethodResourceMapping implements MethodResourceMapping {
private final String rel;
private final Path path;
private final Method method;
private final boolean paging;
private final List<String> parameterNames;
@@ -65,6 +68,7 @@ class RepositoryMethodResourceMapping implements MethodResourceMapping {
annotation.path());
this.method = method;
this.parameterNames = discoverParameterNames(method);
this.paging = Arrays.asList(method.getParameterTypes()).contains(Pageable.class);
}
private static final List<String> discoverParameterNames(Method method) {
@@ -125,4 +129,13 @@ class RepositoryMethodResourceMapping implements MethodResourceMapping {
public List<String> getParameterNames() {
return parameterNames;
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.core.mapping.ResourceMapping#isPagingResource()
*/
@Override
public boolean isPagingResource() {
return paging;
}
}

View File

@@ -44,4 +44,11 @@ public interface ResourceMapping {
* @return will never be {@literal null}.
*/
Path getPath();
/**
* Returns whether the resource is paging one.
*
* @return
*/
boolean isPagingResource();
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013 the original author or authors.
* Copyright 2013-2014 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.
@@ -98,7 +98,7 @@ public class ResourceMappings implements Iterable<ResourceMetadata> {
RepositoryInformation repositoryInformation = repositories.getRepositoryInformationFor(type);
Class<?> repositoryInterface = repositoryInformation.getRepositoryInterface();
CollectionResourceMapping mapping = new RepositoryCollectionResourceMapping(repositoryInterface, relProvider);
CollectionResourceMapping mapping = new RepositoryCollectionResourceMapping(repositoryInformation, relProvider);
RepositoryAwareResourceInformation information = new RepositoryAwareResourceInformation(repositories, mapping,
this, repositoryInformation);
@@ -137,8 +137,8 @@ public class ResourceMappings implements Iterable<ResourceMetadata> {
if (resourceMapping.isExported()) {
for (Method queryMethod : repositoryInformation.getQueryMethods()) {
RepositoryMethodResourceMapping methodMapping = new RepositoryMethodResourceMapping(
queryMethod, resourceMapping);
RepositoryMethodResourceMapping methodMapping = new RepositoryMethodResourceMapping(queryMethod,
resourceMapping);
if (methodMapping.isExported()) {
mappings.add(methodMapping);
}
@@ -305,5 +305,14 @@ public class ResourceMappings implements Iterable<ResourceMetadata> {
return !typeMapping.isExported() ? false : annotation == null ? true : annotation.exported();
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.core.mapping.ResourceMapping#isPagingResource()
*/
@Override
public boolean isPagingResource() {
return false;
}
}
}

View File

@@ -104,6 +104,15 @@ public class SearchResourceMappings implements Iterable<MethodResourceMapping>,
return !mappings.isEmpty();
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.core.mapping.ResourceMapping#isPagingResource()
*/
@Override
public boolean isPagingResource() {
return false;
}
/*
* (non-Javadoc)
* @see java.lang.Iterable#iterator()

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013 the original author or authors.
* Copyright 2013-2014 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.
@@ -106,6 +106,15 @@ class TypeBasedCollectionResourceMapping implements CollectionResourceMapping {
return relProvider.getSingleResourceRelFor(type);
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.core.mapping.CollectionResourceMapping#isPagingResource()
*/
@Override
public boolean isPagingResource() {
return false;
}
/**
* Returns the default path to be used if the path is not configured manually.
*

View File

@@ -1,72 +0,0 @@
package org.springframework.data.rest.core.invoke;
import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.Matchers.*;
import static org.springframework.util.ReflectionUtils.*;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import org.springframework.data.domain.Pageable;
import org.springframework.data.rest.core.domain.jpa.PersonRepository;
import org.springframework.data.rest.core.invoke.RepositoryMethod;
import org.springframework.data.rest.core.support.Methods;
import org.springframework.util.ReflectionUtils;
/**
* Tests to verify the integrity of the {@link RepositoryMethod} abstraction.
*
* @author Jon Brisbin
*/
public class RepositoryMethodUnitTests {
Map<String, RepositoryMethod> methods = new HashMap<String, RepositoryMethod>();
RepositoryMethod method;
@Before
public void setup() {
doWithMethods(PersonRepository.class, new ReflectionUtils.MethodCallback() {
@Override
public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {
String name = method.getName();
RepositoryMethod repoMethod = new RepositoryMethod(method);
methods.put(name, repoMethod);
}
}, Methods.USER_METHODS);
method = methods.get("findByFirstName");
}
@Test
public void shouldFindSimpleQueryMethods() throws Exception {
assertThat(method, notNullValue());
}
@Test
public void shouldFindPageableInformationOnMethod() throws Exception {
assertThat(method, notNullValue());
assertThat(method.isPageable(), is(true));
}
@Test
public void shouldNotFindSortInformationOnMethod() throws Exception {
assertThat(method, notNullValue());
assertThat(method.isSortable(), is(false));
}
@Test
public void shouldProvideParameterClassTypes() throws Exception {
assertThat(method, notNullValue());
assertThat(method.getParameters().get(0).getParameterType(), is(typeCompatibleWith(String.class)));
assertThat(method.getParameters().get(1).getParameterType(), is(typeCompatibleWith(Pageable.class)));
}
@Test
public void shouldProvideParameterNames() throws Exception {
assertThat(method, notNullValue());
assertThat(method.getParameterNames(), contains("firstName", "arg1"));
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013 the original author or authors.
* Copyright 2013-2014 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.
@@ -19,12 +19,13 @@ import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import org.junit.Test;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
import org.springframework.data.rest.core.Path;
import org.springframework.data.rest.core.annotation.RestResource;
import org.springframework.data.rest.core.mapping.CollectionResourceMapping;
import org.springframework.data.rest.core.mapping.RepositoryCollectionResourceMapping;
import org.springframework.data.rest.core.mapping.ResourceMapping;
/**
* Unit tests for {@link RepositoryCollectionResourceMapping}.
@@ -36,7 +37,7 @@ public class RepositoryCollectionResourceMappingUnitTests {
@Test
public void buildsDefaultMappingForRepository() {
CollectionResourceMapping mapping = new RepositoryCollectionResourceMapping(PersonRepository.class);
CollectionResourceMapping mapping = getResourceMappingFor(PersonRepository.class);
assertThat(mapping.getPath(), is(new Path("persons")));
assertThat(mapping.getRel(), is("persons"));
@@ -47,7 +48,7 @@ public class RepositoryCollectionResourceMappingUnitTests {
@Test
public void honorsAnnotatedsMapping() {
CollectionResourceMapping mapping = new RepositoryCollectionResourceMapping(AnnotatedPersonRepository.class);
CollectionResourceMapping mapping = getResourceMappingFor(AnnotatedPersonRepository.class);
assertThat(mapping.getPath(), is(new Path("bar")));
assertThat(mapping.getRel(), is("foo"));
@@ -58,8 +59,7 @@ public class RepositoryCollectionResourceMappingUnitTests {
@Test
public void repositoryAnnotationTrumpsDomainTypeMapping() {
CollectionResourceMapping mapping = new RepositoryCollectionResourceMapping(
AnnotatedAnnotatedPersonRepository.class);
CollectionResourceMapping mapping = getResourceMappingFor(AnnotatedAnnotatedPersonRepository.class);
assertThat(mapping.getPath(), is(new Path("/trumpsAll")));
assertThat(mapping.getRel(), is("foo"));
@@ -70,16 +70,33 @@ public class RepositoryCollectionResourceMappingUnitTests {
@Test
public void doesNotExposeRepositoryForPublicDomainTypeIfRepoIsPackageProtected() {
ResourceMapping mapping = new RepositoryCollectionResourceMapping(PackageProtectedRepository.class);
ResourceMapping mapping = getResourceMappingFor(PackageProtectedRepository.class);
assertThat(mapping.isExported(), is(false));
}
/**
* @see DATAREST-229
*/
@Test
public void detectsPagingRepository() {
assertThat(getResourceMappingFor(PersonRepository.class).isPagingResource(), is(true));
}
private static CollectionResourceMapping getResourceMappingFor(Class<?> repositoryInterface) {
RepositoryMetadata metadata = new DefaultRepositoryMetadata(repositoryInterface);
return new RepositoryCollectionResourceMapping(metadata);
}
public static class Person {}
@RestResource(path = "bar", rel = "foo", exported = false)
static class AnnotatedPerson {}
public interface PersonRepository extends Repository<Person, Long> {}
public interface PersonRepository extends Repository<Person, Long> {
Page<Person> findAll(Pageable pageable);
}
interface AnnotatedPersonRepository extends Repository<AnnotatedPerson, Long> {}
@@ -88,5 +105,5 @@ public class RepositoryCollectionResourceMappingUnitTests {
public static class PublicClass {}
static interface PackageProtectedRepository extends Repository<PublicClass, Long> {}
interface PackageProtectedRepository extends Repository<PublicClass, Long> {}
}

View File

@@ -21,7 +21,11 @@ import static org.junit.Assert.*;
import java.lang.reflect.Method;
import org.junit.Test;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
import org.springframework.data.repository.query.Param;
import org.springframework.data.rest.core.Path;
import org.springframework.data.rest.core.annotation.RestResource;
@@ -33,7 +37,8 @@ import org.springframework.data.rest.core.annotation.RestResource;
*/
public class RepositoryMethodResourceMappingUnitTests {
RepositoryCollectionResourceMapping resourceMapping = new RepositoryCollectionResourceMapping(PersonRepository.class);
RepositoryMetadata metadata = new DefaultRepositoryMetadata(PersonRepository.class);
RepositoryCollectionResourceMapping resourceMapping = new RepositoryCollectionResourceMapping(metadata);
@Test
public void defaultsMappingToMethodName() throws Exception {
@@ -78,6 +83,18 @@ public class RepositoryMethodResourceMappingUnitTests {
assertThat(mapping.getParameterNames(), hasItem("firstname"));
}
/**
* @see DATAREST-229
*/
@Test
public void considersPagingFinderAPagingResource() throws Exception {
Method method = PersonRepository.class.getMethod("findByEmailAddress", String.class, Pageable.class);
MethodResourceMapping mapping = new RepositoryMethodResourceMapping(method, resourceMapping);
assertThat(mapping.isPagingResource(), is(true));
}
static class Person {}
interface PersonRepository extends Repository<Person, Long> {
@@ -89,5 +106,8 @@ public class RepositoryMethodResourceMappingUnitTests {
@RestResource(path = "foo")
Iterable<Person> findByEmailAddress(String email);
@RestResource(path = "fooPaged")
Page<Person> findByEmailAddress(String email, Pageable pageable);
}
}

View File

@@ -244,7 +244,7 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon
*/
@Bean
public EntityLinks entityLinks() {
return new RepositoryEntityLinks(repositories(), resourceMappings(), config());
return new RepositoryEntityLinks(repositories(), resourceMappings(), config(), pageableResolver());
}
/**

View File

@@ -5,12 +5,21 @@ import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
import org.springframework.data.rest.core.mapping.ResourceMappings;
import org.springframework.data.rest.core.mapping.ResourceMetadata;
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.TemplateVariables;
import org.springframework.hateoas.UriTemplate;
import org.springframework.hateoas.core.AbstractEntityLinks;
import org.springframework.util.Assert;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
/**
* {@link EntityLinks} implementation that is able to create {@link Link} for domain classes managed by Spring Data
* REST.
*
* @author Jon Brisbin
* @author Oliver Gierke
*/
@@ -19,15 +28,29 @@ public class RepositoryEntityLinks extends AbstractEntityLinks {
private final Repositories repositories;
private final ResourceMappings mappings;
private final RepositoryRestConfiguration config;
private final HateoasPageableHandlerMethodArgumentResolver resolver;
/**
* Creates a new {@link RepositoryEntityLinks}.
*
* @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}.
*/
@Autowired
public RepositoryEntityLinks(Repositories repositories, ResourceMappings mappings, RepositoryRestConfiguration config) {
public RepositoryEntityLinks(Repositories repositories, ResourceMappings mappings,
RepositoryRestConfiguration config, HateoasPageableHandlerMethodArgumentResolver resolver) {
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!");
this.repositories = repositories;
this.mappings = mappings;
this.config = config;
this.resolver = resolver;
}
/*
@@ -67,6 +90,17 @@ public class RepositoryEntityLinks extends AbstractEntityLinks {
public Link linkToCollectionResource(Class<?> type) {
ResourceMetadata metadata = mappings.getMappingFor(type);
if (metadata.isPagingResource()) {
Link link = linkFor(type).withSelfRel();
String href = link.getHref();
UriComponents components = UriComponentsBuilder.fromUriString(href).build();
TemplateVariables variables = resolver.getPaginationTemplateVariables(null, components);
return new Link(new UriTemplate(href, variables), metadata.getRel());
}
return linkFor(type).withRel(metadata.getRel());
}

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.data.rest.webmvc;
import org.junit.Before;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mapping.PersistentEntity;
@@ -47,6 +48,11 @@ public abstract class AbstractControllerIntegrationTests {
@Autowired RepositoryInvokerFactory invokerFactory;
@Autowired ResourceMappings mappings;
@Before
public void initWebInfrastructure() {
WebTestUtils.initWebTest();
}
/**
* Returns a {@link RepositoryRestRequest} for the given domain type.
*

View File

@@ -87,7 +87,7 @@ public abstract class AbstractWebIntegrationTests {
}
protected MockHttpServletResponse request(Link link) throws Exception {
return request(link.getHref());
return request(link.expand().getHref());
}
protected MockHttpServletResponse request(String href) throws Exception {
@@ -95,7 +95,7 @@ public abstract class AbstractWebIntegrationTests {
}
protected ResultActions follow(Link link) throws Exception {
return follow(link.getHref());
return follow(link.expand().getHref());
}
protected ResultActions follow(String href) throws Exception {
@@ -264,7 +264,7 @@ public abstract class AbstractWebIntegrationTests {
request(link);
// Schema - TODO:Improve by using hypermedia
mvc.perform(get(link.getHref() + "/schema").//
mvc.perform(get(link.expand().getHref() + "/schema").//
accept(MediaType.parseMediaType("application/schema+json"))).//
andExpect(status().isOk());
}
@@ -313,7 +313,7 @@ public abstract class AbstractWebIntegrationTests {
}
@Test
public void postsPayloadToResource() throws Exception {
public void nic() throws Exception {
Map<String, String> payloads = getPayloadToPost();
assumeFalse(payloads.isEmpty());
@@ -325,9 +325,11 @@ public abstract class AbstractWebIntegrationTests {
String payload = payloads.get(rel);
if (payload != null) {
Link link = assertHasLinkWithRel(rel, response);
MockHttpServletRequestBuilder request = post(link.getHref()).//
Link link = assertHasLinkWithRel(rel, response);
String target = link.expand().getHref();
MockHttpServletRequestBuilder request = post(target).//
content(payload).//
contentType(MediaType.APPLICATION_JSON);

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2013 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.rest.webmvc;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
/**
* Helper methods for web integration testing.
*
* @author Oliver Gierke
*/
public class WebTestUtils {
/**
* Initializes web tests. Will register a {@link MockHttpServletRequest} for the current thread.
*/
public static void initWebTest() {
MockHttpServletRequest request = new MockHttpServletRequest();
ServletRequestAttributes requestAttributes = new ServletRequestAttributes(request);
RequestContextHolder.setRequestAttributes(requestAttributes);
}
}

View File

@@ -48,6 +48,7 @@ public class JpaRepositoryConfig {
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
vendorAdapter.setDatabase(Database.HSQL);
vendorAdapter.setGenerateDdl(true);
@@ -55,6 +56,7 @@ public class JpaRepositoryConfig {
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
factory.setJpaVendorAdapter(vendorAdapter);
factory.setPackagesToScan(getClass().getPackage().getName());
factory.setPersistenceUnitName("spring-data-rest-webmvc");
factory.setDataSource(dataSource());
factory.afterPropertiesSet();

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2014 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 static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.rest.webmvc.AbstractControllerIntegrationTests;
import org.springframework.data.rest.webmvc.jpa.JpaRepositoryConfig;
import org.springframework.data.rest.webmvc.jpa.Person;
import org.springframework.hateoas.Link;
import org.springframework.test.context.ContextConfiguration;
/**
* Integration tests for {@link RepositoryEntityLinks}.
*
* @author Oliver Gierke
*/
@ContextConfiguration(classes = JpaRepositoryConfig.class)
public class RepositoryEntityLinksIntegrationTests extends AbstractControllerIntegrationTests {
@Autowired RepositoryEntityLinks entityLinks;
@Test
public void returnsLinkToSingleResource() {
Link link = entityLinks.linkToSingleResource(Person.class, 1);
assertThat(link.getHref(), endsWith("/people/1"));
assertThat(link.getRel(), is("person"));
}
@Test
public void returnsTemplatedLinkForPagingResource() {
Link link = entityLinks.linkToCollectionResource(Person.class);
assertThat(link.isTemplated(), is(true));
assertThat(link.getVariableNames(), hasItems("page", "size", "sort"));
assertThat(link.getRel(), is("people"));
}
}