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

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