From 88b2f05edfa3a6796b9639f21b0e221ff1b39b96 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Wed, 22 Jan 2014 17:54:35 +0100 Subject: [PATCH] DATAREST-229 - RepositoryEntityLinks now exposes templated links. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- pom.xml | 14 +-- spring-data-rest-core/pom.xml | 12 +- .../rest/core/invoke/RepositoryMethod.java | 114 ------------------ .../core/invoke/RepositoryQueryMethod.java | 79 ------------ .../RepositoryAwareResourceInformation.java | 34 +++++- .../RepositoryCollectionResourceMapping.java | 24 +++- .../RepositoryMethodResourceMapping.java | 13 ++ .../rest/core/mapping/ResourceMapping.java | 7 ++ .../rest/core/mapping/ResourceMappings.java | 17 ++- .../core/mapping/SearchResourceMappings.java | 9 ++ .../TypeBasedCollectionResourceMapping.java | 11 +- .../invoke/RepositoryMethodUnitTests.java | 72 ----------- ...oryCollectionResourceMappingUnitTests.java | 39 ++++-- ...ositoryMethodResourceMappingUnitTests.java | 22 +++- .../RepositoryRestMvcConfiguration.java | 2 +- .../webmvc/support/RepositoryEntityLinks.java | 36 +++++- .../AbstractControllerIntegrationTests.java | 6 + .../webmvc/AbstractWebIntegrationTests.java | 14 ++- .../data/rest/webmvc/WebTestUtils.java | 38 ++++++ .../rest/webmvc/jpa/JpaRepositoryConfig.java | 2 + ...RepositoryEntityLinksIntegrationTests.java | 57 +++++++++ 21 files changed, 309 insertions(+), 313 deletions(-) delete mode 100644 spring-data-rest-core/src/main/java/org/springframework/data/rest/core/invoke/RepositoryMethod.java delete mode 100644 spring-data-rest-core/src/main/java/org/springframework/data/rest/core/invoke/RepositoryQueryMethod.java delete mode 100644 spring-data-rest-core/src/test/java/org/springframework/data/rest/core/invoke/RepositoryMethodUnitTests.java create mode 100644 spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/WebTestUtils.java create mode 100644 spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinksIntegrationTests.java diff --git a/pom.xml b/pom.xml index d1093990f..3c86a084d 100644 --- a/pom.xml +++ b/pom.xml @@ -72,13 +72,6 @@ - - - org.springframework.data - spring-data-jpa - ${springdata.jpa} - test - org.hibernate.javax.persistence @@ -94,6 +87,13 @@ test + + org.springframework.data + spring-data-jpa + ${springdata.jpa} + test + + org.springframework.data spring-data-mongodb diff --git a/spring-data-rest-core/pom.xml b/spring-data-rest-core/pom.xml index a316b3519..e5ba45b4b 100644 --- a/spring-data-rest-core/pom.xml +++ b/spring-data-rest-core/pom.xml @@ -28,17 +28,17 @@ spring-tx - - org.springframework.data - spring-data-commons - ${springdata.commons} - - org.springframework.hateoas spring-hateoas ${spring.hateoas} + + + org.springframework.data + spring-data-commons + ${springdata.commons} + org.springframework.plugin diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/invoke/RepositoryMethod.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/invoke/RepositoryMethod.java deleted file mode 100644 index 006634d47..000000000 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/invoke/RepositoryMethod.java +++ /dev/null @@ -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 methodParameters = new ArrayList(); - private List paramNames = new ArrayList(); - 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 getParameters() { - return methodParameters; - } - - /** - * Get the method parameter names. - * - * @return Array of parameter names. - */ - public List 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; - } - -} diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/invoke/RepositoryQueryMethod.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/invoke/RepositoryQueryMethod.java deleted file mode 100644 index 67c4655d5..000000000 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/invoke/RepositoryQueryMethod.java +++ /dev/null @@ -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 - */ -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; - } - -} diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryAwareResourceInformation.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryAwareResourceInformation.java index 8572fdf06..9f167982c 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryAwareResourceInformation.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryAwareResourceInformation.java @@ -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() diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryCollectionResourceMapping.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryCollectionResourceMapping.java index a03a019e3..54298b07e 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryCollectionResourceMapping.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryCollectionResourceMapping.java @@ -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(); + } } diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryMethodResourceMapping.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryMethodResourceMapping.java index 16ec0c08a..b9dcc3fb0 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryMethodResourceMapping.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryMethodResourceMapping.java @@ -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 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 discoverParameterNames(Method method) { @@ -125,4 +129,13 @@ class RepositoryMethodResourceMapping implements MethodResourceMapping { public List getParameterNames() { return parameterNames; } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.mapping.ResourceMapping#isPagingResource() + */ + @Override + public boolean isPagingResource() { + return paging; + } } diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/ResourceMapping.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/ResourceMapping.java index 3d8aed519..32132332b 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/ResourceMapping.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/ResourceMapping.java @@ -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(); } diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/ResourceMappings.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/ResourceMappings.java index 36dcc8648..8ba7dcee2 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/ResourceMappings.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/ResourceMappings.java @@ -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 { 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 { 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 { 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; + } } } diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/SearchResourceMappings.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/SearchResourceMappings.java index 246f927ac..f859ed8d8 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/SearchResourceMappings.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/SearchResourceMappings.java @@ -104,6 +104,15 @@ public class SearchResourceMappings implements Iterable, 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() diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/TypeBasedCollectionResourceMapping.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/TypeBasedCollectionResourceMapping.java index 42b2a3047..d9ef57e14 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/TypeBasedCollectionResourceMapping.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/TypeBasedCollectionResourceMapping.java @@ -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. * diff --git a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/invoke/RepositoryMethodUnitTests.java b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/invoke/RepositoryMethodUnitTests.java deleted file mode 100644 index cccda4954..000000000 --- a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/invoke/RepositoryMethodUnitTests.java +++ /dev/null @@ -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 methods = new HashMap(); - 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")); - } - -} diff --git a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/RepositoryCollectionResourceMappingUnitTests.java b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/RepositoryCollectionResourceMappingUnitTests.java index 12fc7d545..df7973096 100644 --- a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/RepositoryCollectionResourceMappingUnitTests.java +++ b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/RepositoryCollectionResourceMappingUnitTests.java @@ -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 {} + public interface PersonRepository extends Repository { + + Page findAll(Pageable pageable); + } interface AnnotatedPersonRepository extends Repository {} @@ -88,5 +105,5 @@ public class RepositoryCollectionResourceMappingUnitTests { public static class PublicClass {} - static interface PackageProtectedRepository extends Repository {} + interface PackageProtectedRepository extends Repository {} } diff --git a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/RepositoryMethodResourceMappingUnitTests.java b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/RepositoryMethodResourceMappingUnitTests.java index a8baf895c..3593cc67d 100644 --- a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/RepositoryMethodResourceMappingUnitTests.java +++ b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/RepositoryMethodResourceMappingUnitTests.java @@ -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 { @@ -89,5 +106,8 @@ public class RepositoryMethodResourceMappingUnitTests { @RestResource(path = "foo") Iterable findByEmailAddress(String email); + + @RestResource(path = "fooPaged") + Page findByEmailAddress(String email, Pageable pageable); } } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java index a151d2f7a..5092d57be 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java @@ -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()); } /** diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinks.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinks.java index f1829cb31..01f677b5f 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinks.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinks.java @@ -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()); } diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AbstractControllerIntegrationTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AbstractControllerIntegrationTests.java index da93e4b9e..b1059dff3 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AbstractControllerIntegrationTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AbstractControllerIntegrationTests.java @@ -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. * diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AbstractWebIntegrationTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AbstractWebIntegrationTests.java index 2c8443097..f340d83a2 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AbstractWebIntegrationTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AbstractWebIntegrationTests.java @@ -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 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); diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/WebTestUtils.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/WebTestUtils.java new file mode 100644 index 000000000..c56670102 --- /dev/null +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/WebTestUtils.java @@ -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); + } +} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaRepositoryConfig.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaRepositoryConfig.java index f16eaf2b0..d5cf50ccd 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaRepositoryConfig.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaRepositoryConfig.java @@ -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(); diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinksIntegrationTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinksIntegrationTests.java new file mode 100644 index 000000000..7e92eb01f --- /dev/null +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinksIntegrationTests.java @@ -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")); + } +}